| | |
| | | TopToolbar, |
| | | useListContext, |
| | | useNotify, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { |
| | | Alert, |
| | |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <TextInput source="requestId" label="请求ID" />, |
| | | <TextInput source="promptCode" label="Prompt 编码" />, |
| | | <TextInput source="userId" label="用户ID" />, |
| | | <TextInput source="requestId" label="ai.observe.fields.requestId" />, |
| | | <TextInput source="promptCode" label="ai.observe.fields.promptCode" />, |
| | | <TextInput source="userId" label="ai.observe.fields.userId" />, |
| | | <SelectInput |
| | | source="status" |
| | | label="状态" |
| | | label="common.field.status" |
| | | choices={[ |
| | | { id: "RUNNING", name: "RUNNING" }, |
| | | { id: "COMPLETED", name: "COMPLETED" }, |
| | |
| | | ]; |
| | | |
| | | const ObserveSummary = () => { |
| | | const translate = useTranslate(); |
| | | const [stats, setStats] = useState(null); |
| | | const [loading, setLoading] = useState(true); |
| | | const [error, setError] = useState(""); |
| | |
| | | }) |
| | | .catch((err) => { |
| | | if (active) { |
| | | setError(err?.message || "获取 AI 观测统计失败"); |
| | | setError(err?.message || translate("ai.observe.summary.fetchFailed")); |
| | | } |
| | | }) |
| | | .finally(() => { |
| | |
| | | <CardContent> |
| | | <Stack direction="row" justifyContent="space-between" alignItems="center" mb={2}> |
| | | <Box> |
| | | <Typography variant="h6">观测总览</Typography> |
| | | <Typography variant="h6">{translate("ai.observe.summary.title")}</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 当前租户下的 AI 对话调用与 MCP 工具调用统计。 |
| | | {translate("ai.observe.summary.description")} |
| | | </Typography> |
| | | </Box> |
| | | {loading && <CircularProgress size={24} />} |
| | |
| | | {!loading && !error && stats && ( |
| | | <Grid container spacing={2}> |
| | | <Grid item xs={12} md={3}> |
| | | <Typography variant="caption" color="text.secondary">AI 调用量</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.callCount")}</Typography> |
| | | <Typography variant="h5">{stats.callCount ?? 0}</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 成功 {stats.successCount ?? 0} / 失败 {stats.failureCount ?? 0} |
| | | {translate("ai.observe.summary.successFailure", { success: stats.successCount ?? 0, failure: stats.failureCount ?? 0 })} |
| | | </Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <Typography variant="caption" color="text.secondary">平均耗时</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.avgElapsed")}</Typography> |
| | | <Typography variant="h5">{stats.avgElapsedMs ?? 0} ms</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 首包 {stats.avgFirstTokenLatencyMs ?? 0} ms |
| | | {translate("ai.observe.summary.firstToken", { value: stats.avgFirstTokenLatencyMs ?? 0 })} |
| | | </Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <Typography variant="caption" color="text.secondary">Token 使用</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.tokenUsage")}</Typography> |
| | | <Typography variant="h5">{stats.totalTokens ?? 0}</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 平均 {stats.avgTotalTokens ?? 0} |
| | | {translate("ai.observe.summary.avgToken", { value: stats.avgTotalTokens ?? 0 })} |
| | | </Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <Typography variant="caption" color="text.secondary">工具成功率</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.toolSuccessRate")}</Typography> |
| | | <Typography variant="h5">{Number(stats.toolSuccessRate || 0).toFixed(2)}%</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 调用 {stats.toolCallCount ?? 0} / 失败 {stats.toolFailureCount ?? 0} |
| | | {translate("ai.observe.summary.toolCallFailure", { call: stats.toolCallCount ?? 0, failure: stats.toolFailureCount ?? 0 })} |
| | | </Typography> |
| | | </Grid> |
| | | </Grid> |
| | |
| | | ); |
| | | }; |
| | | |
| | | const resolveStatusChip = (status) => { |
| | | const resolveStatusChip = (status, translate) => { |
| | | if (status === "COMPLETED") { |
| | | return { color: "success", label: "成功" }; |
| | | return { color: "success", label: translate("ai.observe.status.completed") }; |
| | | } |
| | | if (status === "FAILED") { |
| | | return { color: "error", label: "失败" }; |
| | | return { color: "error", label: translate("ai.observe.status.failed") }; |
| | | } |
| | | if (status === "ABORTED") { |
| | | return { color: "warning", label: "中断" }; |
| | | return { color: "warning", label: translate("ai.observe.status.aborted") }; |
| | | } |
| | | return { color: "default", label: status || "--" }; |
| | | }; |
| | | |
| | | const AiCallLogDetailDialog = ({ record, open, onClose }) => { |
| | | const notify = useNotify(); |
| | | const translate = useTranslate(); |
| | | const [logs, setLogs] = useState([]); |
| | | const [loading, setLoading] = useState(false); |
| | | |
| | |
| | | }) |
| | | .catch((error) => { |
| | | if (active) { |
| | | notify(error?.message || "获取 MCP 调用日志失败", { type: "error" }); |
| | | notify(error?.message || translate("ai.observe.detail.mcpLogsFailed"), { type: "error" }); |
| | | } |
| | | }) |
| | | .finally(() => { |
| | |
| | | |
| | | return ( |
| | | <Dialog open={open} onClose={onClose} fullWidth maxWidth="lg"> |
| | | <DialogTitle>AI 调用详情</DialogTitle> |
| | | <DialogTitle>{translate("ai.observe.detail.title")}</DialogTitle> |
| | | <DialogContent dividers> |
| | | <Grid container spacing={2}> |
| | | <Grid item xs={12} md={6}> |
| | | <Typography variant="caption" color="text.secondary">请求ID</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.requestId")}</Typography> |
| | | <Typography variant="body2">{record.requestId || "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <Typography variant="caption" color="text.secondary">用户ID</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.userId")}</Typography> |
| | | <Typography variant="body2">{record.userId || "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <Typography variant="caption" color="text.secondary">会话ID</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.sessionId")}</Typography> |
| | | <Typography variant="body2">{record.sessionId || "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={4}> |
| | | <Typography variant="caption" color="text.secondary">Prompt</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.common.prompt")}</Typography> |
| | | <Typography variant="body2">{record.promptName || "--"} / {record.promptCode || "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={4}> |
| | | <Typography variant="caption" color="text.secondary">模型</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.common.model")}</Typography> |
| | | <Typography variant="body2">{record.model || "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={4}> |
| | | <Typography variant="caption" color="text.secondary">状态</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("common.field.status")}</Typography> |
| | | <Typography variant="body2">{record.status || "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <Typography variant="caption" color="text.secondary">MCP 挂载</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.mountedMcp")}</Typography> |
| | | <Typography variant="body2">{record.mountedMcpNames || "--"}</Typography> |
| | | </Grid> |
| | | {record.errorMessage && ( |
| | |
| | | <Grid item xs={12}> |
| | | <Divider sx={{ my: 1 }} /> |
| | | <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1}> |
| | | <Typography variant="h6">MCP 工具调用日志</Typography> |
| | | <Typography variant="h6">{translate("ai.observe.detail.mcpLogs")}</Typography> |
| | | {loading && <CircularProgress size={20} />} |
| | | </Stack> |
| | | {!loading && !logs.length && ( |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 当前调用没有产生 MCP 工具日志。 |
| | | {translate("ai.observe.detail.noMcpLogs")} |
| | | </Typography> |
| | | )} |
| | | <Stack spacing={1.5}> |
| | |
| | | label={item.status || "--"} |
| | | /> |
| | | </Stack> |
| | | <Typography variant="caption" color="text.secondary">输入摘要</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.detail.inputSummary")}</Typography> |
| | | <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> |
| | | {item.inputSummary || "--"} |
| | | </Typography> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1}> |
| | | 输出摘要 / 错误 |
| | | </Typography> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1}>{translate("ai.observe.detail.outputSummary")}</Typography> |
| | | <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> |
| | | {item.outputSummary || item.errorMessage || "--"} |
| | | </Typography> |
| | |
| | | </Grid> |
| | | </DialogContent> |
| | | <DialogActions> |
| | | <Button onClick={onClose}>关闭</Button> |
| | | <Button onClick={onClose}>{translate("ai.common.close")}</Button> |
| | | </DialogActions> |
| | | </Dialog> |
| | | ); |
| | | }; |
| | | |
| | | const AiCallLogCards = ({ onView }) => { |
| | | const translate = useTranslate(); |
| | | const { data, isLoading } = useListContext(); |
| | | const records = useMemo(() => (Array.isArray(data) ? data : []), [data]); |
| | | |
| | |
| | | return ( |
| | | <Box px={2} py={6}> |
| | | <Card variant="outlined" sx={{ p: 3, textAlign: "center", borderStyle: "dashed" }}> |
| | | <Typography variant="subtitle1">暂无 AI 调用日志</Typography> |
| | | <Typography variant="subtitle1">{translate("ai.observe.list.emptyTitle")}</Typography> |
| | | <Typography variant="body2" color="text.secondary" mt={1}> |
| | | 发起 AI 对话后,这里会展示调用统计和审计记录。 |
| | | {translate("ai.observe.list.emptyDescription")} |
| | | </Typography> |
| | | </Card> |
| | | </Box> |
| | |
| | | <Box px={2} py={2}> |
| | | <Grid container spacing={2}> |
| | | {records.map((record) => { |
| | | const statusMeta = resolveStatusChip(record.status); |
| | | const statusMeta = resolveStatusChip(record.status, translate); |
| | | return ( |
| | | <Grid item xs={12} md={6} xl={4} key={record.id}> |
| | | <Card variant="outlined" sx={{ height: "100%", borderRadius: 3, boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)" }}> |
| | | <CardContent sx={{ pb: 1.5 }}> |
| | | <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}> |
| | | <Box> |
| | | <Typography variant="h6">{record.promptName || "AI 对话"}</Typography> |
| | | <Typography variant="h6">{record.promptName || translate("ai.drawer.title")}</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {record.promptCode || "--"} / {record.model || "--"} |
| | | </Typography> |
| | |
| | | <Chip size="small" color={statusMeta.color} label={statusMeta.label} /> |
| | | </Stack> |
| | | <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}> |
| | | <Chip size="small" variant="outlined" label={`用户 ${record.userId || "--"}`} /> |
| | | <Chip size="small" variant="outlined" label={`耗时 ${record.elapsedMs ?? 0} ms`} /> |
| | | <Chip size="small" variant="outlined" label={`Token ${record.totalTokens ?? 0}`} /> |
| | | <Chip size="small" variant="outlined" label={translate("ai.observe.list.userValue", { value: record.userId || "--" })} /> |
| | | <Chip size="small" variant="outlined" label={translate("ai.observe.list.elapsedValue", { value: record.elapsedMs ?? 0 })} /> |
| | | <Chip size="small" variant="outlined" label={translate("ai.observe.list.tokenValue", { value: record.totalTokens ?? 0 })} /> |
| | | </Stack> |
| | | <Divider sx={{ my: 1.5 }} /> |
| | | <Typography variant="caption" color="text.secondary">请求ID</Typography> |
| | | <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.requestId")}</Typography> |
| | | <Typography variant="body2" sx={{ wordBreak: "break-all" }}>{record.requestId || "--"}</Typography> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1.5}> |
| | | MCP / 工具调用 |
| | | </Typography> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.observe.list.mcpToolCalls")}</Typography> |
| | | <Typography variant="body2"> |
| | | 挂载 {record.mountedMcpCount ?? 0} 个,工具成功 {record.toolSuccessCount ?? 0},失败 {record.toolFailureCount ?? 0} |
| | | {translate("ai.observe.list.mcpToolSummary", { |
| | | mcp: record.mountedMcpCount ?? 0, |
| | | success: record.toolSuccessCount ?? 0, |
| | | failure: record.toolFailureCount ?? 0, |
| | | })} |
| | | </Typography> |
| | | {record.errorMessage && ( |
| | | <> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1.5}> |
| | | 错误 |
| | | </Typography> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.common.error")}</Typography> |
| | | <Typography variant="body2">{record.errorMessage}</Typography> |
| | | </> |
| | | )} |
| | | </CardContent> |
| | | <CardActions sx={{ px: 2, pb: 2, pt: 0 }}> |
| | | <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record)}> |
| | | 详情 |
| | | {translate("ai.common.detail")} |
| | | </Button> |
| | | </CardActions> |
| | | </Card> |