Commit 22d9e969 authored by yuguo's avatar yuguo

fix

parent 05686ff8
...@@ -52,7 +52,8 @@ ...@@ -52,7 +52,8 @@
"Bash(wc:*)", "Bash(wc:*)",
"Bash(rm:*)", "Bash(rm:*)",
"Bash(\"E:\\\\internet-hospital\\\\后端审核报告.md\":*)", "Bash(\"E:\\\\internet-hospital\\\\后端审核报告.md\":*)",
"Bash(\"E:\\\\internet-hospital\\\\server\\\\migrations\\\\add_foreign_key_constraints.sql\":*)" "Bash(\"E:\\\\internet-hospital\\\\server\\\\migrations\\\\add_foreign_key_constraints.sql\":*)",
"Bash(sed:*)"
] ]
} }
} }
...@@ -25,7 +25,6 @@ ADD CONSTRAINT fk_doctor_reviews_user ...@@ -25,7 +25,6 @@ ADD CONSTRAINT fk_doctor_reviews_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE doctor_reviews ALTER TABLE doctor_reviews
ADD CONSTRAINT fk_doctor_reviews_doctor
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE; FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE;
-- 3. Consultation相关外键约束 -- 3. Consultation相关外键约束
...@@ -86,7 +85,7 @@ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ...@@ -86,7 +85,7 @@ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE renewal_requests ALTER TABLE renewal_requests
ADD CONSTRAINT fk_renewal_requests_chronic ADD CONSTRAINT fk_renewal_requests_chronic
FOREIGN KEY (chronic_record_id) REFERENCES chronic_records(id) ON DELETE CASCADE; FOREIGN KEY (chronic_id) REFERENCES chronic_records(id) ON DELETE CASCADE;
ALTER TABLE medication_reminders ALTER TABLE medication_reminders
ADD CONSTRAINT fk_medication_reminders_user ADD CONSTRAINT fk_medication_reminders_user
......
...@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
import { import {
Card, Row, Col, Typography, Space, Tabs, Form, Input, Select, Switch, Card, Row, Col, Typography, Space, Tabs, Form, Input, Select, Switch,
Button, Tag, Modal, Divider, Alert, InputNumber, Spin, Button, Tag, Modal, Divider, Alert, InputNumber, Spin,
message, DatePicker, Tooltip, message,
} from 'antd'; } from 'antd';
import { DrawerForm, ProFormText, ProFormSelect, ProFormTextArea, ProTable } from '@ant-design/pro-components'; import { DrawerForm, ProFormText, ProFormSelect, ProFormTextArea, ProTable } from '@ant-design/pro-components';
import { import {
...@@ -14,7 +14,6 @@ import { ...@@ -14,7 +14,6 @@ import {
SafetyOutlined, ExclamationCircleOutlined, SafetyOutlined, ExclamationCircleOutlined,
HistoryOutlined, ReloadOutlined, HistoryOutlined, ReloadOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import dayjs from 'dayjs';
import { adminApi } from '@/api/admin'; import { adminApi } from '@/api/admin';
import type { AIUsageStats, PromptTemplateData } from '@/api/admin'; import type { AIUsageStats, PromptTemplateData } from '@/api/admin';
...@@ -86,14 +85,6 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -86,14 +85,6 @@ const AdminAIConfigPage: React.FC = () => {
const [templateModalType, setTemplateModalType] = useState<'add' | 'edit'>('add'); const [templateModalType, setTemplateModalType] = useState<'add' | 'edit'>('add');
const [editingTemplate, setEditingTemplate] = useState<PromptTemplateData | null>(null); const [editingTemplate, setEditingTemplate] = useState<PromptTemplateData | null>(null);
const [templateSaving, setTemplateSaving] = useState(false); const [templateSaving, setTemplateSaving] = useState(false);
const [logSceneFilter, setLogSceneFilter] = useState<string | undefined>(undefined);
const [logSuccessFilter, setLogSuccessFilter] = useState<string | undefined>(undefined);
const [logDateRange, setLogDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
const [logData, setLogData] = useState<any[]>([]);
const [logLoading, setLogLoading] = useState(false);
const [logTotal, setLogTotal] = useState(0);
const [logPage, setLogPage] = useState(1);
const [logPageSize, setLogPageSize] = useState(20);
useEffect(() => { useEffect(() => {
const fetchConfig = async () => { const fetchConfig = async () => {
...@@ -181,10 +172,6 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -181,10 +172,6 @@ const AdminAIConfigPage: React.FC = () => {
fetchTemplates(); fetchTemplates();
}, []); }, []);
const fetchAILogs = async () => {
// TODO: 实现 AI 日志获取逻辑
console.log('获取 AI 日志', { logSceneFilter, logSuccessFilter, logDateRange });
};
const handleAddTemplate = () => { const handleAddTemplate = () => {
setTemplateModalType('add'); setTemplateModalType('add');
...@@ -417,65 +404,6 @@ const AdminAIConfigPage: React.FC = () => { ...@@ -417,65 +404,6 @@ const AdminAIConfigPage: React.FC = () => {
</Card> </Card>
), ),
}, },
{
key: 'logs',
label: <span><HistoryOutlined /> 模型日志</span>,
children: (
<Card style={{ borderRadius: 12, border: '1px solid #E0F2F1' }}>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space wrap>
<Select placeholder="场景筛选" style={{ width: 150 }} allowClear value={logSceneFilter} onChange={setLogSceneFilter}
options={Object.entries(sceneLabels).map(([k, v]) => ({ label: v, value: k }))} />
<Select placeholder="状态筛选" style={{ width: 120 }} allowClear value={logSuccessFilter} onChange={setLogSuccessFilter}
options={[{ label: '成功', value: 'true' }, { label: '失败', value: 'false' }]} />
<DatePicker.RangePicker value={logDateRange} onChange={(dates) => setLogDateRange(dates)} />
<Button icon={<ReloadOutlined />} onClick={fetchAILogs}>刷新</Button>
</Space>
</div>
<ProTable
search={false}
columns={[
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
{ title: '场景', dataIndex: 'scene', key: 'scene', width: 130, render: (v: string) => <Tag color="blue">{sceneLabels[v] || v}</Tag> },
{ title: '模型', dataIndex: 'model', key: 'model', width: 140 },
{
title: '请求摘要', dataIndex: 'request_content', key: 'request_content', width: 180,
render: (v: string) => <Tooltip title={v}><Text ellipsis style={{ maxWidth: 180, display: 'block' }}>{v || '-'}</Text></Tooltip>,
},
{ title: 'Tokens', key: 'tokens', width: 100, render: (_: any, record: AILogItem) => <Text type="secondary">{record.total_tokens || 0}</Text> },
{ title: '耗时', dataIndex: 'response_time_ms', key: 'response_time_ms', width: 80, render: (v: number) => <Text type="secondary">{v ? (v / 1000).toFixed(2) + 's' : '-'}</Text> },
{
title: '状态', dataIndex: 'success', key: 'success', width: 80,
render: (v: boolean, record: AILogItem) => (
<Space direction="vertical" size={0}>
<Tag color={v ? 'success' : 'error'}>{v ? '成功' : '失败'}</Tag>
{record.is_mock && <Tag color="default">模拟</Tag>}
</Space>
),
},
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 160, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-' },
{
title: '操作', key: 'action', width: 80,
render: (_: any, record: AILogItem) => (
<Button type="link" size="small" onClick={() => { setLogDetailItem(record); setLogDetailVisible(true); }}>详情</Button>
),
},
]}
dataSource={logData}
rowKey="id"
loading={logLoading}
pagination={{
current: logPage,
pageSize: logPageSize,
total: logTotal,
showSizeChanger: true,
showTotal: (t) => `共 ${t} 条`,
onChange: (p, ps) => { setLogPage(p); setLogPageSize(ps); },
}}
/>
</Card>
),
},
]} /> ]} />
<DrawerForm <DrawerForm
......
...@@ -48,7 +48,7 @@ interface TraceDetail { ...@@ -48,7 +48,7 @@ interface TraceDetail {
const token = () => localStorage.getItem('access_token') || ''; const token = () => localStorage.getItem('access_token') || '';
async function fetchAIStats(page: number, agentID: string, startDate?: string, endDate?: string) { async function fetchAIStats(page: number, agentID: string, startDate?: string, endDate?: string) {
const params = new URLSearchParams({ page: String(page), page_size: '20' }); const params = new URLSearchParams({ page: String(page), page_size: '10' });
if (agentID) params.append('agent_id', agentID); if (agentID) params.append('agent_id', agentID);
if (startDate) params.append('start_date', startDate); if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate); if (endDate) params.append('end_date', endDate);
...@@ -56,7 +56,11 @@ async function fetchAIStats(page: number, agentID: string, startDate?: string, e ...@@ -56,7 +56,11 @@ async function fetchAIStats(page: number, agentID: string, startDate?: string, e
headers: { Authorization: `Bearer ${token()}` }, headers: { Authorization: `Bearer ${token()}` },
}); });
const data = await res.json(); const data = await res.json();
return data.data ?? {}; return {
...data.data,
logs: data.data?.recent_logs || [],
total_calls: data.data?.logs_total || 0,
};
} }
async function fetchTrace(traceID: string) { async function fetchTrace(traceID: string) {
...@@ -341,6 +345,41 @@ export default function AILogsPage() { ...@@ -341,6 +345,41 @@ export default function AILogsPage() {
</div> </div>
), ),
}, },
{
key: 'ai-model-logs',
label: <Space><ThunderboltOutlined />AI模型日志</Space>,
children: (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Select placeholder="场景筛选" style={{ width: 150 }} allowClear onChange={setAiAgentFilter}
options={[
{ label: '预问诊对话', value: 'pre_consult_chat' },
{ label: '预问诊报告', value: 'pre_consult_analysis' },
{ label: '鉴别诊断', value: 'consult_diagnosis' },
{ label: '用药建议', value: 'consult_medication' },
{ label: '处方审核', value: 'prescription_review' },
]} />
<RangePicker defaultValue={[dayjs(), dayjs()]} onChange={(dates) => {
setAiDateRange([dates?.[0]?.format('YYYY-MM-DD') || '', dates?.[1]?.format('YYYY-MM-DD') || '']);
}} />
</div>
<Table
dataSource={aiStats?.logs || []}
columns={aiLogColumns}
rowKey="id"
loading={aiLoading}
size="small"
pagination={{
current: aiPage,
pageSize: 20,
total: aiStats?.total_calls || 0,
showTotal: (t) => `共 ${t} 条`,
onChange: (page) => setAiPage(page),
}}
/>
</div>
),
},
{ {
key: 'agent-stats', key: 'agent-stats',
label: <Space><FundOutlined />统计分析</Space>, label: <Space><FundOutlined />统计分析</Space>,
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
UserOutlined, VideoCameraOutlined, SendOutlined, StopOutlined, UserOutlined, VideoCameraOutlined, SendOutlined, StopOutlined,
FileTextOutlined, ThunderboltOutlined, SmileOutlined, FileTextOutlined, ThunderboltOutlined, SmileOutlined,
PictureOutlined, PaperClipOutlined, PictureOutlined, PaperClipOutlined,
SwapOutlined, ClockCircleOutlined, SwapOutlined, ClockCircleOutlined, RobotOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { ConsultMessage } from '@/api/consult'; import type { ConsultMessage } from '@/api/consult';
import { consultApi } from '@/api/consult'; import { consultApi } from '@/api/consult';
...@@ -22,6 +22,14 @@ import { MessageBubble } from './MessageBubble'; ...@@ -22,6 +22,14 @@ import { MessageBubble } from './MessageBubble';
const { Text } = Typography; const { Text } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
const CATEGORY_LABELS: Record<string, string> = {
greeting: '问候',
inquiry: '问诊',
diagnosis: '诊断',
prescription: '处方',
closing: '结束',
};
// 系统级默认快捷回复(fallback) // 系统级默认快捷回复(fallback)
const DEFAULT_REPLIES = [ const DEFAULT_REPLIES = [
{ category: 'greeting', content: '您好,请详细描述一下您的不适情况。' }, { category: 'greeting', content: '您好,请详细描述一下您的不适情况。' },
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment