import { AlertTriangle, Bot, ClipboardList, Database, FileSpreadsheet, FolderOpen, KeyRound, Play, RefreshCcw, Save, Send, Settings, ShieldCheck, Terminal, Trash2, UserRound, Wifi } from "lucide-react"; import { Component, type ErrorInfo, type ReactNode } from "react"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import type { AgentTask, AppConfig, AssistantResultTable, CaptchaResult, DashboardSnapshot, LogEntry, McpRuntimeConfigSnapshot, PriceComparisonExportResult, PriceComparisonRow, SessionStatus, TableExportResult, TaskRunResult } from "../shared/types"; import logoUrl from "./assets/logo.png"; type View = "assistant" | "tasks" | "settings" | "logs"; interface ChatMessage { id: string; role: "user" | "assistant"; text: string; taskIds?: string[]; taskParams?: Record>; comparisonRows?: PriceComparisonRow[]; orderStats?: OrderStatsTableData; mcpTables?: AssistantResultTable[]; attachment?: ChatAttachment; } interface OrderStatsTableData { title: string; rows: OrderStatRow[]; } interface OrderStatRow { metric: string; value: string; note: string; evidence: string; } interface ChatAttachment { kind: "excel"; fileName: string; filePath: string; rowCount: number; tableCount?: number; } interface Feedback { type: "success" | "error" | "info"; text: string; } interface ErrorBoundaryState { error?: Error; } const categoryNames: Record = { system: "系统", member: "会员", points: "积分", coupon: "券", order: "订单", product: "商品", ops: "运维" }; export class AppErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryState> { state: ErrorBoundaryState = {}; static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { error }; } componentDidCatch(error: Error, info: ErrorInfo) { console.error("Renderer crashed", error, info.componentStack); } render() { if (this.state.error) { return (
市民请集合

界面渲染异常

{this.state.error.message || "当前页面遇到异常,已拦截白屏。"}

); } return this.props.children; } } export function App() { const [activeView, setActiveView] = useState("assistant"); const [dashboard, setDashboard] = useState(); const [config, setConfig] = useState(); const [configDraft, setConfigDraft] = useState(); const [session, setSession] = useState({ authenticated: false }); const [tasks, setTasks] = useState([]); const [logs, setLogs] = useState([]); const [selectedTaskId, setSelectedTaskId] = useState("product.price.compare"); const [params, setParams] = useState>({}); const [lastResult, setLastResult] = useState(); const [busy, setBusy] = useState(false); const [chatBusy, setChatBusy] = useState(false); const [authFeedback, setAuthFeedback] = useState(); const [loginFocusTick, setLoginFocusTick] = useState(0); const [prompt, setPrompt] = useState(""); const [messages, setMessages] = useState([ { id: crypto.randomUUID(), role: "assistant", text: "你好,我是市民请集合智能助手。你可以直接说要查会员、导出数据、处理积分或记录一个管理员操作需求,我会先给出可执行动作。" } ]); const selectedTask = useMemo( () => tasks.find((task) => task.id === selectedTaskId) ?? tasks[0], [selectedTaskId, tasks] ); async function refresh() { const [nextDashboard, nextConfig, nextTasks, nextLogs, nextSession] = await Promise.all([ window.smqjhAgent.getDashboard(), window.smqjhAgent.getConfig(), window.smqjhAgent.listTasks(), window.smqjhAgent.listLogs(), window.smqjhAgent.getSession() ]); setDashboard(nextDashboard); setConfig(nextConfig); setConfigDraft((current) => current ?? nextConfig); setTasks(nextTasks); setLogs(nextLogs); setSession(nextSession); } useEffect(() => { void refresh(); return window.smqjhAgent.onLog((entry) => setLogs((current) => [entry, ...current].slice(0, 300))); }, []); async function runTask() { if (!selectedTask) { return; } setBusy(true); try { const result = await window.smqjhAgent.runTask({ taskId: selectedTask.id, params }); setLastResult(result); await refresh(); } finally { setBusy(false); } } async function saveConfig(nextConfig: AppConfig) { const saved = await window.smqjhAgent.saveConfig(nextConfig); setConfig(saved); setConfigDraft(saved); await refresh(); } async function login(request: { username: string; password: string; tenantCode: string; captchaId: string; captchaCode: string }) { setBusy(true); setAuthFeedback({ type: "info", text: "正在登录..." }); try { const nextSession = await window.smqjhAgent.login(request); setSession(nextSession); setAuthFeedback({ type: "success", text: "登录成功" }); await refresh(); } catch (error) { const message = friendlyError(error); setAuthFeedback({ type: "error", text: message || "登录失败" }); } finally { setBusy(false); } } async function resolveProductPriceParams(text: string, currentMessages: ChatMessage[]): Promise | undefined> { const localParams = buildProductPriceParams(text, currentMessages); if (!shouldAskDeepSeekForProductIntent(text, currentMessages)) { return localParams; } try { const result = await window.smqjhAgent.extractIntent({ message: text, history: currentMessages.slice(-8).map((message) => ({ role: message.role, content: message.text })), authenticated: session.authenticated, username: session.username, environmentName: config?.environmentName ?? dashboard?.config.environmentName ?? "test-gateway", baseUrl: config?.baseUrl ?? dashboard?.config.baseUrl ?? "http://192.168.1.242:8080", availableTasks: tasks.map(({ id, title, category, description, dangerLevel }) => ({ id, title, category, description, dangerLevel })) }); const params = normalizeProductIntentParams(result.params, text, currentMessages); if (result.taskId === "product.price.compare" && result.confidence >= 0.5 && params?.productKeyword) { return params; } return undefined; } catch { return localParams; } } async function resolveDatabaseQueryParams(text: string, currentMessages: ChatMessage[]): Promise | undefined> { if (!config?.deepSeek.enabled && !dashboard?.config.deepSeek.enabled) { return undefined; } try { const result = await window.smqjhAgent.extractIntent({ message: text, history: currentMessages.slice(-8).map((message) => ({ role: message.role, content: message.text })), authenticated: session.authenticated, username: session.username, environmentName: config?.environmentName ?? dashboard?.config.environmentName ?? "test-gateway", baseUrl: config?.baseUrl ?? dashboard?.config.baseUrl ?? "http://192.168.1.242:8080", availableTasks: tasks.map(({ id, title, category, description, dangerLevel }) => ({ id, title, category, description, dangerLevel })) }); if (result.taskId !== "database.readonly.query" || result.confidence < 0.55) { return undefined; } const sql = (result.params?.sql ?? "").trim(); if (!sql) { return undefined; } return { question: result.params?.question?.trim() || text, sql }; } catch { return undefined; } } async function sendPrompt(event: FormEvent) { event.preventDefault(); const text = prompt.trim(); if (!text || chatBusy) { return; } const userMessage: ChatMessage = { id: crypto.randomUUID(), role: "user", text }; setPrompt(""); setChatBusy(true); const rowsForExport = isComparisonExportIntent(text) ? getLatestComparisonRows(messages) ?? getComparisonRows(lastResult?.data) : undefined; const tablesForExport = isComparisonExportIntent(text) ? getLatestMcpTables(messages) : undefined; if (rowsForExport) { const actionMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: "收到,我正在把上一轮价格对比结果整理成 Excel 文件。" }; setMessages((current) => [...current, userMessage, actionMessage]); try { const exported = await window.smqjhAgent.exportPriceComparison({ title: buildComparisonExportTitle(rowsForExport), rows: rowsForExport }); setMessages((current) => current.map((message) => message.id === actionMessage.id ? { ...message, text: formatExportResult(exported), attachment: { kind: "excel", fileName: exported.fileName, filePath: exported.filePath, rowCount: exported.rowCount } } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === actionMessage.id ? { ...item, text: `Excel 导出失败:${message || "写入文件失败"}。上一轮表格数据还在当前会话里,可以稍后重试导出。` } : item ) ); } finally { setChatBusy(false); } return; } if (tablesForExport) { const actionMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: "收到,我正在把上一轮表格结果整理成 Excel 文件。" }; setMessages((current) => [...current, userMessage, actionMessage]); try { const exported = await window.smqjhAgent.exportTables({ title: buildTableExportTitle(tablesForExport), tables: tablesForExport }); setMessages((current) => current.map((message) => message.id === actionMessage.id ? { ...message, text: formatTableExportResult(exported), attachment: { kind: "excel", fileName: exported.fileName, filePath: exported.filePath, rowCount: exported.rowCount, tableCount: exported.tableCount } } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === actionMessage.id ? { ...item, text: `Excel 导出失败:${message || "写入文件失败"}。上一轮表格数据还在当前会话里,可以稍后重试导出。` } : item ) ); } finally { setChatBusy(false); } return; } const latestOrderStats = isOrderStatsTableIntent(text) ? getLatestOrderStats(messages) : undefined; if (latestOrderStats) { setMessages((current) => [ ...current, userMessage, { id: crypto.randomUUID(), role: "assistant", text: "已把上一轮订单统计整理成表格视图。", orderStats: latestOrderStats } ]); setChatBusy(false); return; } const thinkingMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: "正在理解需求,并按需要调用 MCP/本地工具..." }; const requestMessages = [...messages, userMessage].slice(-12).map((message) => ({ role: message.role, content: message.text })); setMessages((current) => [...current, userMessage, thinkingMessage]); try { const result = await window.smqjhAgent.runAssistant({ message: text, history: requestMessages, authenticated: session.authenticated, username: session.username, environmentName: config?.environmentName ?? dashboard?.config.environmentName ?? "test-gateway", baseUrl: config?.baseUrl ?? dashboard?.config.baseUrl ?? "http://192.168.1.242:8080" }); const taskIds = result.toolCalls .map((call) => call.name) .filter((name) => tasks.some((task) => task.id === name)); const taskParams = Object.fromEntries( result.toolCalls .filter((call) => tasks.some((task) => task.id === call.name)) .map((call) => [call.name, stringifyParams(call.arguments)]) ); await refresh(); setMessages((current) => current.map((message) => message.id === thinkingMessage.id ? { ...message, text: result.content || "已完成处理,结果如下。", taskIds: taskIds.length ? taskIds : undefined, taskParams, comparisonRows: result.comparisonRows?.length ? result.comparisonRows : undefined, mcpTables: result.tables?.length ? result.tables : undefined } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === thinkingMessage.id ? { ...item, text: `智能体执行失败:${message || "调用失败"}。\n\n我没有继续走前端规则兜底,避免再次出现意图被硬匹配带偏。请检查 MCP 服务、MCP 托管的 DeepSeek 配置和数据库只读配置后重试。` } : item ) ); } finally { setChatBusy(false); } return; const orderCountParams = buildOrderCountParams(text, messages); if (orderCountParams) { const actionMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: "收到,我直接走数据库只读查询订单数量,并排除逻辑删除订单。" }; setMessages((current) => [...current, userMessage, actionMessage]); setSelectedTaskId("order.count.query"); setParams(orderCountParams); setLastResult(undefined); try { const result = await window.smqjhAgent.runTask({ taskId: "order.count.query", params: orderCountParams }); setLastResult(result); await refresh(); setMessages((current) => current.map((message) => message.id === actionMessage.id ? { ...message, text: formatOrderCountResult(result, orderCountParams), orderStats: buildOrderStatsTable(result, orderCountParams) } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === actionMessage.id ? { ...item, text: `订单数量查询启动失败:${message || "调用失败"}。` } : item ) ); } finally { setChatBusy(false); } return; } const productLookupParams = await resolveProductLookupParams(text, messages); if (productLookupParams) { const actionMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: `我判断这个问题需要查商品库,正在按“${productLookupParams.productKeyword}”执行数据库只读查询。`, taskIds: ["product.lookup.summary"], taskParams: { "product.lookup.summary": productLookupParams } }; setMessages((current) => [...current, userMessage, actionMessage]); setSelectedTaskId("product.lookup.summary"); setParams(productLookupParams); setLastResult(undefined); try { const result = await window.smqjhAgent.runTask({ taskId: "product.lookup.summary", params: productLookupParams }); setLastResult(result); await refresh(); setMessages((current) => current.map((message) => message.id === actionMessage.id ? { ...message, text: formatProductLookupResult(result, productLookupParams) } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === actionMessage.id ? { ...item, text: `商品资料查询启动失败:${message || "调用失败"}。` } : item ) ); } finally { setChatBusy(false); } return; } const productPriceParams = await resolveProductPriceParams(text, messages); if (productPriceParams) { const actionMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: `DeepSeek 已识别到商品和比价口径,我按“${productPriceParams.productKeyword}”直接发起商品价格对比调研:先用数据库只读查询 smqjh 系统价,再后台采集苏宁易购和慢慢买公开价格线索。`, taskIds: ["product.price.compare"], taskParams: { "product.price.compare": productPriceParams } }; setMessages((current) => [...current, userMessage, actionMessage]); setSelectedTaskId("product.price.compare"); setParams(productPriceParams); setLastResult(undefined); try { const result = await window.smqjhAgent.runTask({ taskId: "product.price.compare", params: productPriceParams }); setLastResult(result); await refresh(); setMessages((current) => current.map((message) => message.id === actionMessage.id ? { ...message, text: formatProductPriceResult(result, productPriceParams), comparisonRows: getComparisonRows(result.data) } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === actionMessage.id ? { ...item, text: `商品价格对比调研启动失败:${message || "调用失败"}。你也可以点击下方任务手动执行。` } : item ) ); } finally { setChatBusy(false); } return; } const databaseQueryParams = await resolveDatabaseQueryParams(text, messages); if (databaseQueryParams) { const actionMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: "DeepSeek 判断这个问题需要查 smqjh 数据库,我会先做只读 SQL 安全校验,再直接执行并返回表格。", taskIds: ["database.readonly.query"], taskParams: { "database.readonly.query": databaseQueryParams } }; setMessages((current) => [...current, userMessage, actionMessage]); setSelectedTaskId("database.readonly.query"); setParams(databaseQueryParams); setLastResult(undefined); try { const result = await window.smqjhAgent.runTask({ taskId: "database.readonly.query", params: databaseQueryParams }); setLastResult(result); await refresh(); setMessages((current) => current.map((message) => message.id === actionMessage.id ? { ...message, text: formatReadOnlyQueryResult(result, databaseQueryParams) } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === actionMessage.id ? { ...item, text: `数据库智能只读查询启动失败:${message || "调用失败"}。` } : item ) ); } finally { setChatBusy(false); } return; } const localReply = buildAssistantReply(text, tasks, session); const legacyThinkingMessage: ChatMessage = { id: crypto.randomUUID(), role: "assistant", text: "正在分析这个需求..." }; const legacyRequestMessages = [...messages, userMessage].slice(-12).map((message) => ({ role: message.role, content: message.text })); setMessages((current) => [...current, userMessage, legacyThinkingMessage]); try { const result = await window.smqjhAgent.chat({ messages: legacyRequestMessages, authenticated: session.authenticated, username: session.username, environmentName: config?.environmentName ?? dashboard?.config.environmentName ?? "test-gateway", baseUrl: config?.baseUrl ?? dashboard?.config.baseUrl ?? "http://192.168.1.242:8080", availableTasks: tasks.map(({ id, title, category, description, dangerLevel }) => ({ id, title, category, description, dangerLevel })) }); const taskIds = matchTaskIds(text, tasks); setMessages((current) => current.map((message) => message.id === legacyThinkingMessage.id ? { ...message, text: result.content, taskIds: taskIds.length > 0 ? taskIds : localReply.taskIds } : message ) ); } catch (error) { const message = friendlyError(error); setMessages((current) => current.map((item) => item.id === legacyThinkingMessage.id ? { ...item, text: `DeepSeek 暂时没有返回:${message || "调用失败"}\n\n${localReply.text}`, taskIds: localReply.taskIds } : item ) ); } finally { setChatBusy(false); } } function openTask(taskId: string, presetParams?: Record) { setSelectedTaskId(taskId); setParams(presetParams ?? {}); setLastResult(undefined); setActiveView("tasks"); } function openLogin() { setActiveView("settings"); setLoginFocusTick((value) => value + 1); } return (

{activeView === "assistant" ? "市民请集合智能助手" : activeView === "tasks" ? "任务中心" : activeView === "settings" ? "系统配置" : "运行日志"}

{config?.environmentName ?? "test-gateway"} · {config?.baseUrl ?? "http://192.168.1.242:8080"}

{activeView === "assistant" && dashboard && ( )} {activeView === "tasks" && ( { setSelectedTaskId(taskId); setParams({}); setLastResult(undefined); }} params={params} setParams={setParams} runTask={runTask} busy={busy} lastResult={lastResult} /> )} {activeView === "settings" && config && ( { setAuthFeedback(undefined); setSession(await window.smqjhAgent.logout()); }} busy={busy} session={session} feedback={authFeedback} focusTick={loginFocusTick} /> )} {activeView === "logs" && ( { await window.smqjhAgent.clearLogs(); setLogs([]); }} /> )}
); } function matchTaskIds(text: string, tasks: AgentTask[]): string[] { const normalized = text.toLowerCase(); const matchedIds: string[] = []; if (isOrderCountIntent(text)) { matchedIds.push("order.count.query"); } if (isProductLookupIntent(text)) { matchedIds.push("product.lookup.summary"); } if (isProductPriceIntent(text, "")) { matchedIds.push("product.price.compare"); } if (text.includes("管理员") || text.includes("当前用户") || text.includes("当前账号") || normalized.includes("me")) { matchedIds.push("user.current"); } if (text.includes("网关") || text.includes("连通") || text.includes("验证码") || normalized.includes("health")) { matchedIds.push("cloud.health"); } const taskIds = matchedIds.filter((id, index) => matchedIds.indexOf(id) === index && tasks.some((task) => task.id === id)); if (taskIds.length === 0) { taskIds.push(...["product.price.compare", "order.count.query", "cloud.health"].filter((id) => tasks.some((task) => task.id === id))); } return taskIds; } function buildAssistantReply(text: string, tasks: AgentTask[], session: SessionStatus): { text: string; taskIds: string[] } { const taskIds = matchTaskIds(text, tasks); const authText = session.authenticated ? "当前后台账号已登录。" : "当前还没有后台登录,涉及后台接口的动作会先要求登录。"; return { taskIds, text: `${authText} 我理解这个需求可以先走 ${taskIds.length} 个动作:${taskIds.map((id) => tasks.find((task) => task.id === id)?.title ?? id).join("、")}。你可以点右侧动作进入参数确认。` }; } function buildOrderCountParams(text: string, messages: ChatMessage[]): Record | undefined { if (!isOrderCountIntent(text, messages)) { return undefined; } const mobile = extractMobile(text) || getLatestOrderMobile(messages); const dateRange = inferOrderDateRange(text); return { mobile: mobile ?? "", dateFrom: dateRange?.dateFrom ?? "", dateTo: dateRange?.dateTo ?? "" }; } function isOrderCountIntent(text: string, messages: ChatMessage[] = []): boolean { const normalized = text.trim(); if (!normalized) { return false; } const hasOrderWord = /订单|下单|订购|购买|买了|消费记录|交易记录|订单数|订单数量|多少单/.test(normalized); const hasAmountWord = /销售额|营业额|营收|金额|订单金额|实付|支付金额|收款|收入/.test(normalized); const hasCountWord = /多少|几|数量|总数|统计|计数|条|笔|单|次|下了|汇报|汇总/.test(normalized); const followsOrderContext = hasAmountWord && Boolean(getLatestOrderStats(messages)); return (hasOrderWord && (hasCountWord || hasAmountWord)) || hasAmountWord || followsOrderContext; } function extractMobile(text: string): string | undefined { return text.match(/1\d{10}/)?.[0]; } function getLatestOrderMobile(messages: ChatMessage[]): string | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const mobile = extractMobile(messages[index].text); if (mobile) { return mobile; } } return undefined; } function isOrderStatsTableIntent(text: string): boolean { return /表格|表格视图|列表|列出来|整理一下|汇总一下/.test(text) && !isComparisonExportIntent(text); } function getLatestOrderStats(messages: ChatMessage[]): OrderStatsTableData | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { if (messages[index].orderStats?.rows.length) { return messages[index].orderStats; } } return undefined; } function inferOrderDateRange(text: string): { dateFrom: string; dateTo: string } | undefined { const today = startOfLocalDay(new Date()); if (/今天|今日/.test(text)) { return { dateFrom: formatIsoDate(today), dateTo: formatIsoDate(addDays(today, 1)) }; } if (/昨天|昨日/.test(text)) { const yesterday = addDays(today, -1); return { dateFrom: formatIsoDate(yesterday), dateTo: formatIsoDate(today) }; } const recentDays = text.match(/近\s*(\d+)\s*天|最近\s*(\d+)\s*天/); if (recentDays) { const days = Number(recentDays[1] || recentDays[2]); if (Number.isFinite(days) && days > 0) { return { dateFrom: formatIsoDate(addDays(today, -days)), dateTo: formatIsoDate(addDays(today, 1)) }; } } if (/近一年|最近一年|一年内|过去一年/.test(text)) { const from = new Date(today); from.setFullYear(from.getFullYear() - 1); return { dateFrom: formatIsoDate(from), dateTo: formatIsoDate(addDays(today, 1)) }; } if (/本月|这个月|当月/.test(text)) { return { dateFrom: formatIsoDate(new Date(today.getFullYear(), today.getMonth(), 1)), dateTo: formatIsoDate(addDays(today, 1)) }; } if (/今年|本年/.test(text)) { return { dateFrom: formatIsoDate(new Date(today.getFullYear(), 0, 1)), dateTo: formatIsoDate(addDays(today, 1)) }; } if (/去年|上一年/.test(text)) { return { dateFrom: formatIsoDate(new Date(today.getFullYear() - 1, 0, 1)), dateTo: formatIsoDate(new Date(today.getFullYear(), 0, 1)) }; } const explicitRange = text.match(/(\d{4}-\d{1,2}-\d{1,2}).{0,8}(?:到|至|-|~).{0,8}(\d{4}-\d{1,2}-\d{1,2})/); if (explicitRange) { return { dateFrom: normalizeDateText(explicitRange[1]), dateTo: formatIsoDate(addDays(parseLocalDate(normalizeDateText(explicitRange[2])), 1)) }; } return undefined; } function formatOrderCountResult(result: TaskRunResult, params: Record): string { if (!result.success) { return `订单数量查询未完成:${result.message}`; } const data = result.data && typeof result.data === "object" ? (result.data as Record) : {}; const scope = buildOrderScopeLabel(params); return `已通过 MySQL 只读查询${scope},订单总数:${numberText(data.totalCount)} 单,已支付销售额:${moneyText(data.paidAmount)}。下方已整理成表格视图。`; } function buildOrderStatsTable(result: TaskRunResult, params: Record): OrderStatsTableData | undefined { if (!result.success || !result.data || typeof result.data !== "object") { return undefined; } const data = result.data as Record; const scope = buildOrderScopeLabel(params); const evidence = String(data.evidence ?? "").trim(); const rows: OrderStatRow[] = [ { metric: "订单总数", value: `${numberText(data.totalCount)} 单`, note: "满足当前筛选条件且未逻辑删除的订单数量", evidence }, { metric: "订单金额合计", value: moneyText(data.totalAmount), note: `金额口径:${String(data.amountField ?? "COALESCE(actual_total, total, order_money)")}`, evidence }, { metric: "已支付销售额", value: moneyText(data.paidAmount), note: "is_payed = 1 的订单金额合计", evidence: "SUM(CASE WHEN is_payed = 1 THEN orderAmount ELSE 0 END)" }, { metric: "未支付金额", value: moneyText(data.unpaidAmount), note: "is_payed = 0 或为空的订单金额合计", evidence: "SUM(CASE WHEN COALESCE(is_payed, 0) = 0 THEN orderAmount ELSE 0 END)" }, { metric: "已完成金额", value: moneyText(data.completedAmount), note: "完成口径订单金额合计", evidence: "hb_order_status IN (80, 90, 100)" }, { metric: "已取消金额", value: moneyText(data.canceledAmount), note: "取消口径订单金额合计", evidence: "hb_order_status IN (-1, 50, 60)" } ]; if (params.mobile) { rows.push( { metric: "收货手机号匹配", value: `${numberText(data.consigneeMobileCount)} 单`, note: `consignee_mobile 匹配 ${params.mobile}`, evidence: `oms_order.consignee_mobile LIKE "%${params.mobile}%"` }, { metric: "会员手机号匹配", value: `${numberText(data.memberMobileCount)} 单`, note: `会员手机号或权益账号手机号匹配 ${params.mobile}`, evidence: `sm_member.mobile / ums_member_account.mobile LIKE "%${params.mobile}%"` }, { metric: "买家手机号匹配", value: `${numberText(data.buyerMobileCount)} 单`, note: `买家会员手机号匹配 ${params.mobile}`, evidence: `sm_member.mobile LIKE "%${params.mobile}%"` } ); } rows.push( { metric: "已支付", value: `${numberText(data.paidCount)} 单`, note: "is_payed = 1", evidence: "COUNT DISTINCT order_id" }, { metric: "未支付", value: `${numberText(data.unpaidCount)} 单`, note: "is_payed = 0 或为空", evidence: "COUNT DISTINCT order_id" }, { metric: "已完成", value: `${numberText(data.completedCount)} 单`, note: "hb_order_status 属于完成口径", evidence: "hb_order_status IN (80, 90, 100)" }, { metric: "已取消", value: `${numberText(data.canceledCount)} 单`, note: "hb_order_status 属于取消口径", evidence: "hb_order_status IN (-1, 50, 60)" } ); const latestOrderTime = String(data.latestOrderTime ?? "").trim(); if (latestOrderTime) { rows.push({ metric: "最近下单时间", value: latestOrderTime, note: "当前筛选范围内 create_time 最大值", evidence: "MAX(oms_order.create_time)" }); } return { title: `订单统计${scope}`, rows }; } function buildOrderScopeLabel(params: Record): string { const parts: string[] = []; if (params.mobile) { parts.push(`手机号 ${params.mobile}`); } if (params.dateFrom && params.dateTo) { parts.push(`${params.dateFrom} 至 ${formatIsoDate(addDays(parseLocalDate(params.dateTo), -1))}`); } else if (params.dateFrom) { parts.push(`${params.dateFrom} 起`); } else if (params.dateTo) { parts.push(`${formatIsoDate(addDays(parseLocalDate(params.dateTo), -1))} 前`); } return parts.length ? `(${parts.join(",")})` : "全部非删除订单"; } function numberText(value: unknown): string { const number = Number(value); return Number.isFinite(number) ? String(number) : "0"; } function moneyText(value: unknown): string { const number = Number(value); return Number.isFinite(number) ? `¥${number.toFixed(2)}` : "¥0.00"; } function startOfLocalDay(date: Date): Date { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function addDays(date: Date, days: number): Date { const next = new Date(date); next.setDate(next.getDate() + days); return next; } function formatIsoDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function normalizeDateText(value: string): string { return value .split("-") .map((part, index) => (index === 0 ? part.padStart(4, "0") : part.padStart(2, "0"))) .join("-"); } function parseLocalDate(value: string): Date { const [year, month, day] = normalizeDateText(value).split("-").map(Number); return new Date(year, month - 1, day); } async function resolveProductLookupParams(text: string, currentMessages: ChatMessage[]): Promise | undefined> { return buildProductLookupParams(text, currentMessages); } function buildProductLookupParams(text: string, messages: ChatMessage[]): Record | undefined { if (!isProductLookupIntent(text)) { return undefined; } const keyword = extractProductKeyword(text) || getLatestComparisonProduct(messages); return keyword ? { productKeyword: keyword } : undefined; } function isProductLookupIntent(text: string): boolean { if (isObviousNonProductTopic(text)) { return false; } const hasProductContext = /商品|产品|sku|spu|商城|业务系统|系统|后台|smqjh/i.test(text); const hasLookupWord = /描述|说明|详情|资料|信息|简介|规格|品牌|状态|叫什么|是什么|查一下|查询|查看/.test(text); const keyword = extractProductKeyword(text); return Boolean(keyword) && hasLookupWord && (hasProductContext || /ml|mL|ML|毫升|瓶|罐|盒|箱|可乐|饮料|牛奶|茶|水/.test(text)); } function formatProductLookupResult(result: TaskRunResult, params: Record): string { if (!result.success) { return `商品资料查询未完成:${result.message}。`; } const data = result.data && typeof result.data === "object" ? (result.data as Record) : {}; return [ `已通过 MySQL 只读查询“${params.productKeyword}”的商品资料。`, "", "| 项目 | 内容 |", "| --- | --- |", `| 商品名称 | ${escapeMarkdownCell(String(data.productName ?? ""))} |`, `| 品牌 | ${escapeMarkdownCell(String(data.brandName ?? "未设置"))} |`, `| 价格 | ${escapeMarkdownCell(String(data.price ?? "未获取"))} |`, `| 状态 | ${escapeMarkdownCell(String(data.status ?? "未设置"))} |`, `| 商品描述 | ${escapeMarkdownCell(String(data.description ?? "未读取到描述"))} |`, `| 备注 | ${escapeMarkdownCell(String(data.note ?? ""))} |`, `| 依据 | ${escapeMarkdownCell(String(data.evidence ?? ""))} |` ].join("\n"); } function escapeMarkdownCell(value: string): string { return value .replace(/\|/g, "\\|") .replace(/\r?\n/g, " ") .trim(); } function formatReadOnlyQueryResult(result: TaskRunResult, params: Record): string { if (!result.success) { const data = result.data && typeof result.data === "object" ? (result.data as Record) : {}; const reason = String(data.reason ?? result.message ?? "只读查询失败"); const sql = String(data.sql ?? params.sql ?? "").trim(); return [ `数据库智能只读查询未完成:${reason}`, sql ? `依据 SQL:${sql}` : "" ] .filter(Boolean) .join("\n\n"); } const data = result.data && typeof result.data === "object" ? (result.data as Record) : {}; const columns = Array.isArray(data.columns) ? data.columns.map((column) => String(column)) : []; const rows = Array.isArray(data.rows) ? data.rows.filter((row): row is Record => Boolean(row) && typeof row === "object") : []; const executedSql = String(data.executedSql ?? params.sql ?? "").trim(); const rowCount = Number(data.rowCount ?? rows.length); return [ `已根据你的问题执行数据库只读查询,共返回 ${Number.isFinite(rowCount) ? rowCount : rows.length} 行。`, toMarkdownResultTable(columns, rows), executedSql ? `依据 SQL:${executedSql}` : "" ] .filter(Boolean) .join("\n\n"); } function toMarkdownResultTable(columns: string[], rows: Record[]): string { if (columns.length === 0) { return "| 结果 | 说明 |\n| --- | --- |\n| 无返回行 | 查询执行成功,但结果集为空 |"; } const displayColumns = columns.slice(0, 12); const overflowCount = Math.max(columns.length - displayColumns.length, 0); const header = `| ${displayColumns.map(escapeMarkdownCell).join(" | ")}${overflowCount > 0 ? " | 其他列 |" : " |"}`; const divider = `| ${displayColumns.map(() => "---").join(" | ")}${overflowCount > 0 ? " | --- |" : " |"}`; const body = rows.slice(0, 100).map((row) => { const cells = displayColumns.map((column) => escapeMarkdownCell(formatQueryCell(row[column]))); if (overflowCount > 0) { cells.push(`还有 ${overflowCount} 列未展开`); } return `| ${cells.join(" | ")} |`; }); return [header, divider, ...body].join("\n"); } function formatQueryCell(value: unknown): string { if (value === null || value === undefined) { return ""; } if (value instanceof Date) { return value.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false }); } if (typeof value === "object") { return JSON.stringify(value); } return String(value); } function buildProductPriceParams(text: string, messages: ChatMessage[]): Record | undefined { const standaloneIntent = isStandaloneProductPriceIntent(text); const followUpIntent = !standaloneIntent && isProductPriceFollowUp(text, messages); if (!standaloneIntent && !followUpIntent) { return undefined; } const productFromCurrent = extractProductKeyword(text); const productFromContext = followUpIntent ? getLatestComparisonProduct(messages) : ""; let productKeyword = productFromCurrent || productFromContext; if (!productKeyword) { return undefined; } const spec = inferProductSpec(text); if (spec && !productKeyword.includes(spec)) { productKeyword = `${productKeyword} ${spec}`; } return { productKeyword, productType: "auto", openBrowser: /打开浏览器|弹出页面|人工复核|手动查看/.test(text) ? "true" : "false", notes: spec ? `按${spec}口径查询,对比同规格到手价。` : "按同规格、同口径到手价进行对比。" }; } function shouldAskDeepSeekForProductIntent(text: string, messages: ChatMessage[]): boolean { return isStandaloneProductPriceIntent(text) || isProductPriceFollowUp(text, messages); } function normalizeProductIntentParams( params: Record | undefined, text: string, messages: ChatMessage[] ): Record | undefined { if (!params) { return undefined; } const fallback = buildProductPriceParams(text, messages); if (!fallback) { return undefined; } const rawKeyword = (params.productKeyword || params.keyword || "").trim(); const keyword = extractProductKeyword(rawKeyword) || rawKeyword || fallback.productKeyword || ""; if (!keyword) { return undefined; } const spec = inferProductSpec(`${text} ${keyword}`); const productKeyword = spec && !keyword.includes(spec) ? `${keyword} ${spec}` : keyword; const notes = (params.notes || "").trim() || (spec ? `按${spec}口径查询,对比同规格到手价。` : "按同规格、同口径到手价进行对比。"); return { productKeyword, productType: params.productType || "auto", openBrowser: String(params.openBrowser).toLowerCase() === "true" || /打开浏览器|弹出页面|人工复核|手动查看/.test(text) ? "true" : "false", notes }; } function isProductPriceIntent(text: string, historyText: string): boolean { return isStandaloneProductPriceIntent(text) || isStandaloneProductPriceIntent(historyText); } function isStandaloneProductPriceIntent(text: string): boolean { if (isObviousNonProductTopic(text)) { return false; } const keyword = extractProductKeyword(text); if (!keyword || isGenericProductKeyword(keyword)) { return false; } const hasPriceWord = /价格|定价|售价|比价|到手价|多少钱|对比|比较|电商价|系统价/.test(text); const hasProductContext = /商品|产品|系统价|业务系统|sku|spu|淘宝|京东|拼多多|电商|市面|平台|商城|同规格|smqjh/i.test(text); const hasConcreteSpec = Boolean(inferProductSpec(text)) || /单瓶|一瓶|1瓶|单件|一件|箱装|整箱|瓶装|盒装|罐装|ml|mL|ML|毫升|升|L\b/.test(text); return hasPriceWord && (hasProductContext || hasConcreteSpec); } function isProductPriceFollowUp(text: string, messages: ChatMessage[]): boolean { if (isObviousNonProductTopic(text) || !getLatestComparisonProduct(messages)) { return false; } const looksLikeFollowUp = /^(再|继续|换成|改成|按|只|也|那|这个|这款|它|同规格|单瓶|单件|一瓶|一件)/.test(text.trim()) || /单瓶|一瓶|1瓶|单件|一件|同规格|口径|规格|\d+(?:\.\d+)?\s*(?:ml|mL|ML|毫升|l|L|升)/.test(text); const hasPriceAction = /价格|定价|售价|比价|到手价|多少钱|对比|比较|查|看|跑/.test(text); return looksLikeFollowUp && hasPriceAction; } function isObviousNonProductTopic(text: string): boolean { if (/天气|气温|下雨/.test(text)) { return true; } const hasPriceOrProduct = /商品|产品|价格|定价|售价|比价|到手价|多少钱|系统价|电商|淘宝|京东|拼多多|市面/.test(text); return !hasPriceOrProduct && /订单|订购|支付|退款|售后|物流|快递|会员|手机号|登录|验证码|日志|网关|接口|token|权限|菜单|配置|数据库|报错|启动|退出/.test(text); } function isGenericProductKeyword(keyword: string): boolean { return /^(单瓶|一瓶|1瓶|单件|一件|单个|规格|同规格|口径|价格|比价|对比|好|好的|行|可以|只|这个|这款|它)$/i.test(keyword.trim()); } function getLatestComparisonProduct(messages: ChatMessage[]): string { const rows = getLatestComparisonRows(messages); if (!rows) { return ""; } const systemRow = rows.find((row) => row.platform === "smqjh系统" && row.productName && row.price !== "未获取"); return (systemRow ?? rows.find((row) => row.productName))?.productName ?? ""; } function inferProductSpec(text: string): string { if (/单瓶|一瓶|1瓶/.test(text)) { return "单瓶"; } const volume = text.match(/\d+(?:\.\d+)?\s*(?:ml|mL|ML|毫升|l|L|升)(?:\s*[xX*×]\s*\d+\s*(?:瓶|桶|罐|支|盒)?)?/); return volume?.[0].replace(/\s+/g, "") ?? ""; } function extractProductKeyword(text: string): string { const beforeCompare = text .replace(/(?:和|与|跟|同|及|以及)?\s*(?:其它|其他|其余)?\s*(?:电商平台|电商|平台|淘宝|京东|拼多多|市面).*$/s, " ") .replace(/市面上.*$/s, " ") .replace(/其它?电商平台.*$/s, " "); const cleaned = beforeCompare .replace(/https?:\/\/\S+/g, " ") .replace(/[??。!,,;;::\n\r]/g, " ") .replace(/(?:帮我|麻烦|我要|我想|可以)?\s*(?:在|从)?\s*(?:业务系统|系统|后台|smqjh)(?:里面|里|中|的)?/gi, " ") .replace(/我们|咱们|我方|我司|本方|己方|我要|我想|帮我|麻烦|可以|一下|这个|某个|当前|只/g, " ") .replace(/(?:只)?(?:查询一下|查一下|查询|查看|查|看看)/g, " ") .replace(/商品描述|商品说明|商品资料|商品信息|描述|说明|资料|信息|简介|详情|是什么|还是什么|商品|产品|价格是多少|定价是多少|售价是多少|多少钱|价格|定价|售价|到手价|比价|对比一下|对比下|对比|比较一下|比较下|比较|做个|做一下/g, " ") .replace(/淘宝|京东|拼多多|电商平台|电商|市面|平台/g, " ") .replace(/的/g, " ") .replace(/\s+/g, " ") .replace(/^(?:在|从|里|里面|中)\s+/g, "") .replace(/^(?:和|与|跟|同|及|以及)\s*/g, "") .replace(/\s*(?:和|与|跟|同|及|以及)$/g, "") .trim(); const genericOnly = /^(单瓶|一瓶|1瓶|单个|规格|同规格|口径|好|好的|行|可以|只)$/; return genericOnly.test(cleaned) ? "" : cleaned; } function formatProductPriceResult(result: TaskRunResult, params: Record): string { if (!result.success) { return `商品价格对比调研未完成:${result.message}。我已把“${params.productKeyword}”填入任务参数,可以在任务页调整后重新运行。`; } const data = result.data && typeof result.data === "object" ? (result.data as Record) : {}; const rows = getComparisonRows(result.data) ?? []; const externalRows = rows.filter((row) => row.platform !== "smqjh系统"); const blockedCount = externalRows.filter((row) => row.status === "未采集到").length; const systemMissing = rows.some((row) => row.platform === "smqjh系统" && row.price === "未获取"); const browserResults = Array.isArray(data.browserResults) ? data.browserResults : []; const openedCount = browserResults.filter((item) => Boolean((item as { opened?: boolean }).opened)).length; const browserText = openedCount > 0 ? `另外按要求打开了 ${openedCount} 个外部平台页面。` : "未弹出外部平台页面。"; const systemText = systemMissing ? "smqjh 系统价暂未匹配到,通常需要更精确的商品ID、SKU ID、商品编码,或确认后台商品名称。" : "已拿到 smqjh 系统价。"; const marketText = blockedCount === externalRows.length && externalRows.length > 0 ? "外部平台公开页面触发登录/风控或没有可解析价格,这不是你还需要手工查,而是需要接入平台官方接口、采购接口或授权采集账号。" : "外部平台已尽量后台采集,表格里会标出采集状态。"; return [ `已按“${params.productKeyword}”完成商品价格对比调研,${browserText}`, `${systemText}${marketText}`, "下方是表格视图,“依据”列会标出数据库只读查询范围或外部浏览器请求地址。" ] .filter(Boolean) .join("\n\n"); } function isComparisonExportIntent(text: string): boolean { const normalized = text.toLowerCase(); const hasExportAction = /导出|下载|保存|生成|做成|做个|做一个|整理成|转成|给我一个|给我份/.test(text); const hasSheetTarget = /excel|xlsx|xls|exlc|表格|电子表格|文件/.test(normalized); return hasExportAction && hasSheetTarget; } function getLatestComparisonRows(messages: ChatMessage[]): PriceComparisonRow[] | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const rows = messages[index].comparisonRows; if (rows?.length) { return rows; } } return undefined; } function getLatestMcpTables(messages: ChatMessage[]): AssistantResultTable[] | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const tables = messages[index].mcpTables?.filter((table) => table.columns.length > 0); if (tables?.length) { return tables; } } return undefined; } function buildComparisonExportTitle(rows: PriceComparisonRow[]): string { const systemRow = rows.find((row) => row.platform === "smqjh系统") ?? rows[0]; return `商品价格对比_${systemRow?.productName || "导出"}`; } function buildTableExportTitle(tables: AssistantResultTable[]): string { const title = tables[0]?.title?.trim(); return title ? `智能体查询结果_${title}` : "智能体查询结果"; } function formatExportResult(result: PriceComparisonExportResult): string { return `已经按上一轮价格对比结果生成 Excel:${result.fileName}\n\n共导出 ${result.rowCount} 行数据,文件已保存到本机下载目录。`; } function formatTableExportResult(result: TableExportResult): string { return `已经把上一轮表格结果生成 Excel:${result.fileName}\n\n共导出 ${result.tableCount} 个表、${result.rowCount} 行数据,文件已保存到本机下载目录。`; } function NavButton(props: { active: boolean; icon: JSX.Element; label: string; onClick: () => void }) { return ( ); } function StatusPill({ ok, label }: { ok: boolean; label: string }) { return {label}; } function friendlyError(error: unknown): string { const raw = error instanceof Error ? error.message : String(error); const cleaned = raw .replace(/^Error invoking remote method '[^']+': Error: /, "") .replace(/^Error: /, "") .trim(); if (cleaned === "Bad credentials") { return "登录失败:账号、密码或验证码错误"; } return cleaned; } function stringifyParams(params: Record): Record { return Object.fromEntries( Object.entries(params).map(([key, value]) => [key, value === undefined || value === null ? "" : String(value)]) ); } function AssistantView(props: { messages: ChatMessage[]; prompt: string; setPrompt: (value: string) => void; sendPrompt: (event: FormEvent) => void; chatBusy: boolean; dashboard: DashboardSnapshot; session: SessionStatus; tasks: AgentTask[]; onPickTask: (taskId: string, presetParams?: Record) => void; }) { const reviewTasks = props.tasks.filter((task) => task.dangerLevel === "review").length; const conversationEndRef = useRef(null); useEffect(() => { conversationEndRef.current?.scrollIntoView({ block: "end", behavior: "smooth" }); }, [props.messages, props.chatBusy]); return (
{props.messages.map((message) => (
{message.role === "assistant" ? : }
{message.comparisonRows && } {message.orderStats && } {message.mcpTables && } {message.attachment && } {message.taskIds && (
{message.taskIds.map((taskId) => { const task = props.tasks.find((item) => item.id === taskId); return task ? ( ) : null; })}
)}
))} {props.chatBusy && (
正在处理
)}