ToolsCallLogDetail.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import React, { useState, useEffect } from "react";
  2. import { Card, Descriptions, Button, message, Tag, Spin } from "antd";
  3. import { ArrowLeftOutlined, CopyOutlined } from "@ant-design/icons";
  4. import { useParams, useNavigate } from "react-router-dom";
  5. import { toolsCallLogApi } from "../services/api";
  6. import dayjs from "dayjs";
  7. const ToolsCallLogDetail = () => {
  8. const { id } = useParams();
  9. const navigate = useNavigate();
  10. const [loading, setLoading] = useState(false);
  11. const [data, setData] = useState(null);
  12. // 格式化时间戳为 YYYY-MM-DD HH:mm:ss
  13. const formatTimestamp = (timestamp) => {
  14. if (!timestamp) return "-";
  15. return dayjs(timestamp).format("YYYY-MM-DD HH:mm:ss");
  16. };
  17. // 计算耗时(秒)
  18. const calculateDuration = (callTime, finishTime) => {
  19. if (!callTime || !finishTime) return "-";
  20. return ((finishTime - callTime) / 1000).toFixed(2) + "s";
  21. };
  22. // 复制到剪贴板
  23. const copyToClipboard = async (text) => {
  24. try {
  25. await navigator.clipboard.writeText(text);
  26. message.success("复制成功");
  27. } catch (error) {
  28. message.error("复制失败");
  29. }
  30. };
  31. // 获取详情数据
  32. const fetchDetail = async () => {
  33. setLoading(true);
  34. try {
  35. const response = await toolsCallLogApi.getDetail(id);
  36. setData(response.data);
  37. } catch (error) {
  38. message.error("获取详情失败");
  39. console.error("获取工具调用日志详情失败:", error);
  40. } finally {
  41. setLoading(false);
  42. }
  43. };
  44. // 返回列表
  45. const handleBack = () => {
  46. navigate("/tools-call-log");
  47. };
  48. useEffect(() => {
  49. if (id) {
  50. fetchDetail();
  51. }
  52. }, [id]);
  53. if (loading) {
  54. return (
  55. <div style={{ padding: "24px", textAlign: "center" }}>
  56. <Spin size="large" />
  57. </div>
  58. );
  59. }
  60. if (!data) {
  61. return (
  62. <div style={{ padding: "24px" }}>
  63. <Button icon={<ArrowLeftOutlined />} onClick={handleBack}>
  64. 返回列表
  65. </Button>
  66. <div style={{ textAlign: "center", marginTop: "50px" }}>
  67. <p>未找到相关数据</p>
  68. </div>
  69. </div>
  70. );
  71. }
  72. return (
  73. <div style={{ padding: "24px" }}>
  74. <div style={{ marginBottom: "16px" }}>
  75. <Button icon={<ArrowLeftOutlined />} onClick={handleBack}>
  76. 返回列表
  77. </Button>
  78. </div>
  79. <Card title="工具调用日志详情">
  80. <Descriptions bordered column={2}>
  81. <Descriptions.Item label="用户名" span={1}>
  82. {data.user || "-"}
  83. </Descriptions.Item>
  84. <Descriptions.Item label="工具名称" span={1}>
  85. {data.mcp_tools_name || "-"}
  86. </Descriptions.Item>
  87. <Descriptions.Item label="Token" span={2}>
  88. <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
  89. <span style={{ wordBreak: "break-all" }}>{data.token || "-"}</span>
  90. {data.token && (
  91. <Button
  92. type="text"
  93. size="small"
  94. icon={<CopyOutlined />}
  95. onClick={() => copyToClipboard(data.token)}
  96. title="复制Token"
  97. />
  98. )}
  99. </div>
  100. </Descriptions.Item>
  101. <Descriptions.Item label="状态" span={1}>
  102. <Tag color={data.status === "success" ? "green" : "red"}>
  103. {data.status === "success" ? "成功" : "失败"}
  104. </Tag>
  105. </Descriptions.Item>
  106. <Descriptions.Item label="耗时" span={1}>
  107. {calculateDuration(data.call_timestamp, data.finish_timestamp)}
  108. </Descriptions.Item>
  109. <Descriptions.Item label="调用时间" span={1}>
  110. {formatTimestamp(data.call_timestamp)}
  111. </Descriptions.Item>
  112. <Descriptions.Item label="完成时间" span={1}>
  113. {formatTimestamp(data.finish_timestamp)}
  114. </Descriptions.Item>
  115. {data.fail_msg && (
  116. <Descriptions.Item label="失败原因" span={2}>
  117. <div style={{ color: "#ff4d4f", wordBreak: "break-word" }}>
  118. {data.fail_msg}
  119. </div>
  120. </Descriptions.Item>
  121. )}
  122. </Descriptions>
  123. </Card>
  124. {/* 请求参数 */}
  125. <Card title="请求参数" style={{ marginTop: "16px" }}>
  126. <div style={{ position: "relative" }}>
  127. <Button
  128. type="text"
  129. size="small"
  130. icon={<CopyOutlined />}
  131. onClick={() => copyToClipboard(data.request_params || "")}
  132. style={{ position: "absolute", top: "8px", right: "8px", zIndex: 1 }}
  133. title="复制请求参数"
  134. />
  135. <pre
  136. style={{
  137. background: "#f5f5f5",
  138. padding: "12px",
  139. borderRadius: "4px",
  140. overflow: "auto",
  141. maxHeight: "300px",
  142. margin: 0,
  143. fontSize: "12px",
  144. lineHeight: "1.4",
  145. }}
  146. >
  147. {data.request_params || "无请求参数"}
  148. </pre>
  149. </div>
  150. </Card>
  151. {/* 工具响应 */}
  152. <Card title="工具响应" style={{ marginTop: "16px" }}>
  153. <div style={{ position: "relative" }}>
  154. <Button
  155. type="text"
  156. size="small"
  157. icon={<CopyOutlined />}
  158. onClick={() => copyToClipboard(data.response || "")}
  159. style={{ position: "absolute", top: "8px", right: "8px", zIndex: 1 }}
  160. title="复制工具响应"
  161. />
  162. <pre
  163. style={{
  164. background: "#f5f5f5",
  165. padding: "12px",
  166. borderRadius: "4px",
  167. overflow: "auto",
  168. maxHeight: "300px",
  169. margin: 0,
  170. fontSize: "12px",
  171. lineHeight: "1.4",
  172. }}
  173. >
  174. {data.response || "无响应数据"}
  175. </pre>
  176. </div>
  177. </Card>
  178. </div>
  179. );
  180. };
  181. export default ToolsCallLogDetail;