@yuku123/z-agent-frontend-component 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/z-agent-frontend-component.css +1 -0
- package/dist/z-agent-frontend-component.es.js +9956 -0
- package/dist/z-agent-frontend-component.umd.js +219 -0
- package/package.json +77 -0
- package/src/api/apiRouter.js +78 -0
- package/src/api/index.js +23 -0
- package/src/api/request.js +59 -0
- package/src/api/routes.js +140 -0
- package/src/dev.jsx +80 -0
- package/src/index.js +86 -0
- package/src/pages/agent/app/index.jsx +2 -0
- package/src/pages/agent/editor/AgentAppEditor.jsx +456 -0
- package/src/pages/agent/editor/WorkflowEditor.jsx +495 -0
- package/src/pages/agent/editor/nodes/index.ts +225 -0
- package/src/pages/agent/index.jsx +1379 -0
- package/src/pages/agent/share.jsx +512 -0
- package/src/pages/ak/AkUsageDrawer.jsx +208 -0
- package/src/pages/ak/index.jsx +496 -0
- package/src/pages/llm/index.jsx +736 -0
- package/src/pages/llm/model/index.jsx +220 -0
- package/src/pages/llm/provider/index.jsx +173 -0
- package/src/pages/mcp/index.jsx +359 -0
- package/src/pages/oss/BucketList.jsx +320 -0
- package/src/pages/oss/ObjectBrowser.jsx +409 -0
- package/src/pages/product/execute.jsx +608 -0
- package/src/pages/product/index.jsx +628 -0
- package/src/pages/product/scene.jsx +746 -0
- package/src/pages/script/ApiBridgeEditor.jsx +255 -0
- package/src/pages/script/CurlImportModal.jsx +263 -0
- package/src/pages/script/FieldMappingEditor.jsx +131 -0
- package/src/pages/script/OpenApiImportModal.jsx +212 -0
- package/src/pages/script/index.jsx +532 -0
- package/src/pages/skill/index.jsx +1595 -0
- package/src/pages/trace/DebugPlayground.jsx +357 -0
- package/src/pages/trace/components/MetricsDashboard.jsx +164 -0
- package/src/pages/trace/components/RagFragments.jsx +134 -0
- package/src/pages/trace/components/Timeline.jsx +142 -0
- package/src/pages/trace/components/ToolCallTree.jsx +116 -0
- package/src/pages/trace/index.jsx +13 -0
- package/src/pages/usage/index.jsx +352 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Empty, Tag, Tooltip} from 'antd';
|
|
3
|
+
import {
|
|
4
|
+
BulbOutlined,
|
|
5
|
+
CodeOutlined,
|
|
6
|
+
FileSearchOutlined,
|
|
7
|
+
MailOutlined,
|
|
8
|
+
PlayCircleOutlined,
|
|
9
|
+
StopOutlined,
|
|
10
|
+
ThunderboltOutlined,
|
|
11
|
+
} from '@ant-design/icons';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Timeline 组件 (FEATURE013 T7)
|
|
15
|
+
*
|
|
16
|
+
* <p>按 sequence 排序展示该 trace 的所有 span. 每个 span 一行:
|
|
17
|
+
* <ul>
|
|
18
|
+
* <li>序号 + 类型图标 + 类型 Tag</li>
|
|
19
|
+
* <li>span 名称 + 耗时</li>
|
|
20
|
+
* <li>content 摘要 (think 文本 / tool_call 参数 / retrieve chunks)</li>
|
|
21
|
+
* <li>attributes 详情 (折叠展开)</li>
|
|
22
|
+
* </ul>
|
|
23
|
+
*/
|
|
24
|
+
const TYPE_META = {
|
|
25
|
+
think: {color: 'gold', icon: <BulbOutlined/>, label: '思考'},
|
|
26
|
+
llm_call: {color: 'blue', icon: <ThunderboltOutlined/>, label: 'LLM'},
|
|
27
|
+
tool_call: {color: 'orange', icon: <CodeOutlined/>, label: '工具'},
|
|
28
|
+
retrieve: {color: 'green', icon: <FileSearchOutlined/>, label: 'RAG'},
|
|
29
|
+
observe: {color: 'purple', icon: <MailOutlined/>, label: '观察'},
|
|
30
|
+
step: {color: 'default', icon: <PlayCircleOutlined/>, label: '步骤'},
|
|
31
|
+
flow_node: {color: 'cyan', icon: <PlayCircleOutlined/>, label: 'Flow节点'},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const STATUS_META = {
|
|
35
|
+
RUNNING: {color: 'processing', icon: <PlayCircleOutlined/>},
|
|
36
|
+
SUCCESS: {color: 'success', icon: <PlayCircleOutlined/>},
|
|
37
|
+
FAILURE: {color: 'error', icon: <StopOutlined/>},
|
|
38
|
+
ERROR: {color: 'error', icon: <StopOutlined/>},
|
|
39
|
+
PENDING: {color: 'default', icon: null},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const Timeline = ({spans = []}) => {
|
|
43
|
+
if (!spans.length) {
|
|
44
|
+
return (
|
|
45
|
+
<Empty
|
|
46
|
+
description="暂无 span 数据"
|
|
47
|
+
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
|
|
54
|
+
{spans.map((span, idx) => {
|
|
55
|
+
const meta = TYPE_META[span.spanType] || {color: 'default', icon: null, label: span.spanType};
|
|
56
|
+
const statusMeta = STATUS_META[span.status] || STATUS_META.PENDING;
|
|
57
|
+
// 解析 attributes
|
|
58
|
+
let attrs = null;
|
|
59
|
+
if (span.attributes) {
|
|
60
|
+
try { attrs = typeof span.attributes === 'string' ? JSON.parse(span.attributes) : span.attributes; } catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
const isError = span.status === 'FAILURE' || span.status === 'ERROR';
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
key={span.spanId || idx}
|
|
66
|
+
style={{
|
|
67
|
+
border: '1px solid #f0f0f0',
|
|
68
|
+
borderLeft: `3px solid ${isError ? '#ff4d4f' : '#1677ff'}`,
|
|
69
|
+
borderRadius: 6,
|
|
70
|
+
padding: '10px 12px',
|
|
71
|
+
background: '#fafafa',
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{/* Header 行 */}
|
|
75
|
+
<div style={{
|
|
76
|
+
display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6,
|
|
77
|
+
}}>
|
|
78
|
+
<span style={{
|
|
79
|
+
width: 22, height: 22, borderRadius: 11, background: '#1677ff',
|
|
80
|
+
color: '#fff', display: 'inline-flex', alignItems: 'center',
|
|
81
|
+
justifyContent: 'center', fontSize: 11, fontWeight: 600,
|
|
82
|
+
}}>{idx + 1}</span>
|
|
83
|
+
<Tag color={meta.color} icon={meta.icon}>{meta.label}</Tag>
|
|
84
|
+
<Tag color={statusMeta.color} icon={statusMeta.icon}>{span.status}</Tag>
|
|
85
|
+
<span style={{fontSize: 12, color: '#666'}}>{span.name}</span>
|
|
86
|
+
<div style={{flex: 1}}/>
|
|
87
|
+
<Tooltip title="耗时">
|
|
88
|
+
<Tag>{span.durationMs != null ? `${span.durationMs}ms` : '—'}</Tag>
|
|
89
|
+
</Tooltip>
|
|
90
|
+
<span style={{fontSize: 11, color: '#999'}}>
|
|
91
|
+
#{span.sequence}
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
{/* Content 行 */}
|
|
95
|
+
{span.content && (
|
|
96
|
+
<div style={{
|
|
97
|
+
fontSize: 12,
|
|
98
|
+
color: '#333',
|
|
99
|
+
background: '#fff',
|
|
100
|
+
padding: '6px 8px',
|
|
101
|
+
borderRadius: 4,
|
|
102
|
+
border: '1px dashed #e8e8e8',
|
|
103
|
+
marginBottom: attrs ? 6 : 0,
|
|
104
|
+
whiteSpace: 'pre-wrap',
|
|
105
|
+
wordBreak: 'break-word',
|
|
106
|
+
maxHeight: 120,
|
|
107
|
+
overflow: 'auto',
|
|
108
|
+
}}>
|
|
109
|
+
{truncate(span.content, 500)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
{/* Attributes 详情 */}
|
|
113
|
+
{attrs && Object.keys(attrs).length > 0 && (
|
|
114
|
+
<details style={{fontSize: 11, color: '#888'}}>
|
|
115
|
+
<summary style={{cursor: 'pointer', userSelect: 'none'}}>
|
|
116
|
+
查看 attributes ({Object.keys(attrs).length})
|
|
117
|
+
</summary>
|
|
118
|
+
<pre style={{
|
|
119
|
+
background: '#fff', padding: 6, marginTop: 4,
|
|
120
|
+
borderRadius: 4, maxHeight: 150, overflow: 'auto',
|
|
121
|
+
}}>{JSON.stringify(attrs, null, 2)}</pre>
|
|
122
|
+
</details>
|
|
123
|
+
)}
|
|
124
|
+
{/* Error message */}
|
|
125
|
+
{span.errorMessage && (
|
|
126
|
+
<div style={{marginTop: 6, fontSize: 12, color: '#ff4d4f'}}>
|
|
127
|
+
❌ {span.errorMessage}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
function truncate(s, max) {
|
|
138
|
+
if (!s) return s;
|
|
139
|
+
return s.length > max ? s.substring(0, max) + '…' : s;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default Timeline;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Empty, Tag} from 'antd';
|
|
3
|
+
import {BranchesOutlined} from '@ant-design/icons';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ToolCallTree 组件 (FEATURE013 T7)
|
|
7
|
+
*
|
|
8
|
+
* <p>树形展示 tool_call 类型 span, 按 parent_span_id 嵌套.
|
|
9
|
+
* 每个节点展示: 工具名 / 参数 / 结果 / 耗时 / 状态.
|
|
10
|
+
*/
|
|
11
|
+
const ToolCallTree = ({spans = []}) => {
|
|
12
|
+
// 过滤出 tool_call / llm_call 类型
|
|
13
|
+
const callSpans = spans.filter(s =>
|
|
14
|
+
s.spanType === 'tool_call' || s.spanType === 'llm_call',
|
|
15
|
+
);
|
|
16
|
+
if (!callSpans.length) {
|
|
17
|
+
return (
|
|
18
|
+
<Empty
|
|
19
|
+
description="本次执行没有工具/LLM 调用"
|
|
20
|
+
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 构建 map
|
|
26
|
+
const byId = new Map(callSpans.map(s => [s.spanId, s]));
|
|
27
|
+
// 找根节点 (parent_span_id 不在 callSpans 里 或为 null)
|
|
28
|
+
const roots = callSpans.filter(s => !s.parentSpanId || !byId.has(s.parentSpanId));
|
|
29
|
+
|
|
30
|
+
const renderNode = (span, depth) => {
|
|
31
|
+
let args = null, result = null;
|
|
32
|
+
try {
|
|
33
|
+
const attrs = span.attributes
|
|
34
|
+
? (typeof span.attributes === 'string' ? JSON.parse(span.attributes) : span.attributes)
|
|
35
|
+
: null;
|
|
36
|
+
if (attrs) {
|
|
37
|
+
args = attrs.arguments || attrs.argumentsJson || attrs.tool_input;
|
|
38
|
+
result = attrs.tool_result || attrs.result || attrs.output;
|
|
39
|
+
}
|
|
40
|
+
} catch { /* ignore */ }
|
|
41
|
+
|
|
42
|
+
const children = callSpans.filter(s => s.parentSpanId === span.spanId);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div key={span.spanId} style={{
|
|
46
|
+
marginLeft: depth * 20, marginBottom: 8,
|
|
47
|
+
borderLeft: depth > 0 ? '2px solid #d9d9d9' : 'none',
|
|
48
|
+
paddingLeft: depth > 0 ? 12 : 0,
|
|
49
|
+
}}>
|
|
50
|
+
<div style={{
|
|
51
|
+
background: '#fff',
|
|
52
|
+
border: '1px solid #e8e8e8',
|
|
53
|
+
borderRadius: 6,
|
|
54
|
+
padding: '8px 12px',
|
|
55
|
+
borderLeft: `3px solid ${span.spanType === 'tool_call' ? '#fa8c16' : '#1677ff'}`,
|
|
56
|
+
}}>
|
|
57
|
+
<div style={{display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4}}>
|
|
58
|
+
<BranchesOutlined/>
|
|
59
|
+
<Tag color={span.spanType === 'tool_call' ? 'orange' : 'blue'}>
|
|
60
|
+
{span.spanType === 'tool_call' ? 'Tool' : 'LLM'}
|
|
61
|
+
</Tag>
|
|
62
|
+
<span style={{fontWeight: 600}}>{span.name}</span>
|
|
63
|
+
<Tag color={
|
|
64
|
+
span.status === 'SUCCESS' ? 'success' :
|
|
65
|
+
span.status === 'FAILURE' ? 'error' : 'default'
|
|
66
|
+
}>{span.status}</Tag>
|
|
67
|
+
<div style={{flex: 1}}/>
|
|
68
|
+
<span style={{fontSize: 11, color: '#999'}}>
|
|
69
|
+
{span.durationMs != null ? `${span.durationMs}ms` : ''}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
{args && (
|
|
73
|
+
<details>
|
|
74
|
+
<summary style={{cursor: 'pointer', fontSize: 12, color: '#666'}}>
|
|
75
|
+
📥 入参
|
|
76
|
+
</summary>
|
|
77
|
+
<pre style={{
|
|
78
|
+
background: '#fafafa', padding: 6, marginTop: 4,
|
|
79
|
+
borderRadius: 4, fontSize: 11, maxHeight: 100, overflow: 'auto',
|
|
80
|
+
}}>{typeof args === 'string' ? args : JSON.stringify(args, null, 2)}</pre>
|
|
81
|
+
</details>
|
|
82
|
+
)}
|
|
83
|
+
{result && (
|
|
84
|
+
<details>
|
|
85
|
+
<summary style={{cursor: 'pointer', fontSize: 12, color: '#666'}}>
|
|
86
|
+
📤 结果
|
|
87
|
+
</summary>
|
|
88
|
+
<pre style={{
|
|
89
|
+
background: '#f6ffed', padding: 6, marginTop: 4,
|
|
90
|
+
borderRadius: 4, fontSize: 11, maxHeight: 150, overflow: 'auto',
|
|
91
|
+
}}>{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}</pre>
|
|
92
|
+
</details>
|
|
93
|
+
)}
|
|
94
|
+
{span.errorMessage && (
|
|
95
|
+
<div style={{fontSize: 12, color: '#ff4d4f', marginTop: 4}}>
|
|
96
|
+
❌ {span.errorMessage}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
{children.length > 0 && (
|
|
101
|
+
<div style={{marginTop: 6}}>
|
|
102
|
+
{children.map(c => renderNode(c, depth + 1))}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
{roots.map(r => renderNode(r, 0))}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default ToolCallTree;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Outlet} from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Agent Trace 路由壳子 (FEATURE013 T7)
|
|
6
|
+
* 子路由:
|
|
7
|
+
* - /ai/trace/debug -> DebugPlayground (默认重定向)
|
|
8
|
+
*/
|
|
9
|
+
const TraceIndex = () => {
|
|
10
|
+
return <Outlet/>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default TraceIndex;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
import {Card, Col, Row, Select, Statistic, Table, Tabs, Tag, Typography} from 'antd'
|
|
3
|
+
import {BarChartOutlined, DollarOutlined, RocketOutlined} from '@ant-design/icons'
|
|
4
|
+
import request from '@/api/request'
|
|
5
|
+
import ReactECharts from 'echarts-for-react'
|
|
6
|
+
|
|
7
|
+
const {Text} = Typography
|
|
8
|
+
const {TabPane} = Tabs
|
|
9
|
+
|
|
10
|
+
const UsageDashboard = () => {
|
|
11
|
+
const [overview, setOverview] = useState(null)
|
|
12
|
+
const [byApp, setByApp] = useState([])
|
|
13
|
+
const [byUser, setByUser] = useState([])
|
|
14
|
+
const [trend, setTrend] = useState([])
|
|
15
|
+
const [records, setRecords] = useState([])
|
|
16
|
+
const [loading, setLoading] = useState(false)
|
|
17
|
+
|
|
18
|
+
// 维度筛选
|
|
19
|
+
const [appFilter, setAppFilter] = useState(null)
|
|
20
|
+
const [providerFilter, setProviderFilter] = useState(null)
|
|
21
|
+
const [modelFilter, setModelFilter] = useState(null)
|
|
22
|
+
const [providers, setProviders] = useState([])
|
|
23
|
+
const [models, setModels] = useState([])
|
|
24
|
+
const [apps, setApps] = useState([])
|
|
25
|
+
|
|
26
|
+
// 加载维度下拉数据
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
// 加载供应商
|
|
29
|
+
request('/llm-center/provider/list').then(res => {
|
|
30
|
+
const list = Array.isArray(res) ? res : (res?.data || [])
|
|
31
|
+
setProviders(list)
|
|
32
|
+
}).catch(() => {
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// 加载模型(支持按供应商过滤)
|
|
36
|
+
loadModels(null)
|
|
37
|
+
|
|
38
|
+
// 加载应用列表
|
|
39
|
+
request('/agent/app/page', {method: 'POST', body: JSON.stringify({current: 1, size: 100})}).then(res => {
|
|
40
|
+
const list = Array.isArray(res) ? res : (res?.records || res?.data || [])
|
|
41
|
+
setApps(list)
|
|
42
|
+
}).catch(() => {
|
|
43
|
+
})
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
const loadModels = (providerCode) => {
|
|
47
|
+
request('/llm-center/model/list', {params: {providerCode}}).then(res => {
|
|
48
|
+
const list = Array.isArray(res) ? res : (res?.data || [])
|
|
49
|
+
setModels(list)
|
|
50
|
+
}).catch(() => {
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleProviderChange = (val) => {
|
|
55
|
+
setProviderFilter(val)
|
|
56
|
+
setModelFilter(null)
|
|
57
|
+
loadModels(val)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const buildFilterParams = () => ({
|
|
61
|
+
appCode: appFilter,
|
|
62
|
+
providerCode: providerFilter,
|
|
63
|
+
modelCode: modelFilter,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const fetchOverview = async () => {
|
|
67
|
+
try {
|
|
68
|
+
const res = await request('/llm-gateway/usage/overview', {
|
|
69
|
+
method: 'GET',
|
|
70
|
+
params: buildFilterParams()
|
|
71
|
+
})
|
|
72
|
+
setOverview(res?.data || res || null)
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.warn('Usage overview API unavailable:', e?.message)
|
|
75
|
+
setOverview(null)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fetchByApp = async () => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await request('/llm-gateway/usage/by-app', {method: 'GET'})
|
|
82
|
+
setByApp(Array.isArray(res) ? res : (res?.data || []))
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.warn('Usage by-app API unavailable:', e?.message)
|
|
85
|
+
setByApp([])
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fetchByUser = async () => {
|
|
90
|
+
try {
|
|
91
|
+
const res = await request('/llm-gateway/usage/by-user', {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
params: buildFilterParams()
|
|
94
|
+
})
|
|
95
|
+
setByUser(Array.isArray(res) ? res : (res?.data || []))
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.warn('Usage by-user API unavailable:', e?.message)
|
|
98
|
+
setByUser([])
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const fetchTrend = async () => {
|
|
103
|
+
try {
|
|
104
|
+
const res = await request('/llm-gateway/usage/trend', {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
params: {...buildFilterParams(), days: 7}
|
|
107
|
+
})
|
|
108
|
+
setTrend(Array.isArray(res) ? res : (res?.data || []))
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.warn('Usage trend API unavailable:', e?.message)
|
|
111
|
+
setTrend([])
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fetchRecords = async () => {
|
|
116
|
+
setLoading(true)
|
|
117
|
+
try {
|
|
118
|
+
const res = await request('/llm-gateway/usage/records', {
|
|
119
|
+
method: 'GET',
|
|
120
|
+
params: {...buildFilterParams(), page: 1, size: 20}
|
|
121
|
+
})
|
|
122
|
+
const records = Array.isArray(res) ? res : (res?.records || res?.data || [])
|
|
123
|
+
setRecords(records)
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.warn('Usage records API unavailable:', e?.message)
|
|
126
|
+
setRecords([])
|
|
127
|
+
} finally {
|
|
128
|
+
setLoading(false)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
fetchOverview()
|
|
134
|
+
fetchByApp()
|
|
135
|
+
fetchByUser()
|
|
136
|
+
fetchTrend()
|
|
137
|
+
fetchRecords()
|
|
138
|
+
}, [appFilter, providerFilter, modelFilter])
|
|
139
|
+
|
|
140
|
+
// 趋势图配置
|
|
141
|
+
const trendOption = {
|
|
142
|
+
tooltip: {trigger: 'axis'},
|
|
143
|
+
legend: {data: ['Token消耗', '调用次数']},
|
|
144
|
+
xAxis: {
|
|
145
|
+
type: 'category',
|
|
146
|
+
data: trend.map(t => t.date)
|
|
147
|
+
},
|
|
148
|
+
yAxis: [
|
|
149
|
+
{type: 'value', name: 'Tokens', position: 'left'},
|
|
150
|
+
{type: 'value', name: '调用次数', position: 'right'}
|
|
151
|
+
],
|
|
152
|
+
series: [
|
|
153
|
+
{
|
|
154
|
+
name: 'Token消耗',
|
|
155
|
+
type: 'bar',
|
|
156
|
+
data: trend.map(t => t.dailyTokens)
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: '调用次数',
|
|
160
|
+
type: 'line',
|
|
161
|
+
yAxisIndex: 1,
|
|
162
|
+
data: trend.map(t => t.dailyCalls)
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const appColumns = [
|
|
168
|
+
{title: '应用编码', dataIndex: 'appCode'},
|
|
169
|
+
{title: '调用次数', dataIndex: 'totalCalls', render: v => v?.toLocaleString()},
|
|
170
|
+
{title: '输入Token', dataIndex: 'inputTokens', render: v => v?.toLocaleString()},
|
|
171
|
+
{title: '输出Token', dataIndex: 'outputTokens', render: v => v?.toLocaleString()},
|
|
172
|
+
{title: '总Token', dataIndex: 'totalTokens', render: v => v?.toLocaleString()},
|
|
173
|
+
{
|
|
174
|
+
title: '费用',
|
|
175
|
+
dataIndex: 'totalCost',
|
|
176
|
+
render: v => <Text style={{color: '#faad14'}}>¥{v?.toFixed(4) || '0'}</Text>
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
const userColumns = [
|
|
181
|
+
{title: '用户ID', dataIndex: 'userId'},
|
|
182
|
+
{title: '调用次数', dataIndex: 'totalCalls', render: v => v?.toLocaleString()},
|
|
183
|
+
{title: '总Token', dataIndex: 'totalTokens', render: v => v?.toLocaleString()},
|
|
184
|
+
{
|
|
185
|
+
title: '费用',
|
|
186
|
+
dataIndex: 'totalCost',
|
|
187
|
+
render: v => <Text style={{color: '#faad14'}}>¥{v?.toFixed(4) || '0'}</Text>
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
const recordColumns = [
|
|
192
|
+
{title: '时间', dataIndex: 'gmtCreate', width: 160},
|
|
193
|
+
{title: '应用', dataIndex: 'appCode', render: v => <Tag color="blue">{v || '-'}</Tag>},
|
|
194
|
+
{title: '用户', dataIndex: 'userName'},
|
|
195
|
+
{title: '供应商', dataIndex: 'providerCode', render: v => <Tag color="purple">{v || '-'}</Tag>},
|
|
196
|
+
{title: '模型', dataIndex: 'modelCode', render: v => <Tag>{v || '-'}</Tag>},
|
|
197
|
+
{title: '输入Token', dataIndex: 'inputTokens', render: v => v?.toLocaleString()},
|
|
198
|
+
{title: '输出Token', dataIndex: 'outputTokens', render: v => v?.toLocaleString()},
|
|
199
|
+
{title: '延迟', dataIndex: 'latencyMs', render: v => v ? `${v}ms` : '-'},
|
|
200
|
+
{
|
|
201
|
+
title: '状态',
|
|
202
|
+
dataIndex: 'status',
|
|
203
|
+
render: v => <Tag color={v === 'SUCCESS' ? 'green' : 'red'}>{v || '-'}</Tag>
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
title: '费用',
|
|
207
|
+
dataIndex: 'totalCost',
|
|
208
|
+
render: v => <Text style={{color: '#faad14'}}>¥{v || 0}</Text>
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div>
|
|
214
|
+
{/* 维度筛选栏 */}
|
|
215
|
+
<div style={{
|
|
216
|
+
display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap',
|
|
217
|
+
background: '#fff', padding: '12px 16px', borderRadius: 10,
|
|
218
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.06)', marginBottom: 16
|
|
219
|
+
}}>
|
|
220
|
+
<span style={{fontWeight: 600, color: '#333', marginRight: 4}}>维度筛选:</span>
|
|
221
|
+
<Select
|
|
222
|
+
placeholder="筛选应用"
|
|
223
|
+
allowClear
|
|
224
|
+
style={{width: 180}}
|
|
225
|
+
value={appFilter}
|
|
226
|
+
onChange={setAppFilter}
|
|
227
|
+
options={apps.map(a => ({value: a.appCode, label: a.appName || a.appCode}))}
|
|
228
|
+
/>
|
|
229
|
+
<Select
|
|
230
|
+
placeholder="筛选供应商"
|
|
231
|
+
allowClear
|
|
232
|
+
style={{width: 180}}
|
|
233
|
+
value={providerFilter}
|
|
234
|
+
onChange={handleProviderChange}
|
|
235
|
+
options={providers.map(p => ({value: p.providerCode, label: p.providerName}))}
|
|
236
|
+
/>
|
|
237
|
+
<Select
|
|
238
|
+
placeholder="筛选模型"
|
|
239
|
+
allowClear
|
|
240
|
+
style={{width: 180}}
|
|
241
|
+
value={modelFilter}
|
|
242
|
+
onChange={setModelFilter}
|
|
243
|
+
options={models.map(m => ({value: m.modelCode, label: m.modelName}))}
|
|
244
|
+
/>
|
|
245
|
+
{(appFilter || providerFilter || modelFilter) && (
|
|
246
|
+
<a onClick={() => {
|
|
247
|
+
setAppFilter(null);
|
|
248
|
+
setProviderFilter(null);
|
|
249
|
+
setModelFilter(null)
|
|
250
|
+
}} style={{fontSize: 12}}>
|
|
251
|
+
清空筛选
|
|
252
|
+
</a>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{/* 统计卡片 */}
|
|
257
|
+
<Row gutter={16} style={{marginBottom: 16}}>
|
|
258
|
+
<Col span={6}>
|
|
259
|
+
<Card size="small" style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
260
|
+
<Statistic
|
|
261
|
+
title={<span style={{color: '#666', fontSize: 13}}>累计 Token</span>}
|
|
262
|
+
value={overview?.totalTokens || 0}
|
|
263
|
+
prefix={<RocketOutlined style={{color: '#667eea'}}/>}
|
|
264
|
+
formatter={v => v?.toLocaleString()}
|
|
265
|
+
valueStyle={{color: '#333', fontSize: 22, fontWeight: 700}}
|
|
266
|
+
/>
|
|
267
|
+
</Card>
|
|
268
|
+
</Col>
|
|
269
|
+
<Col span={6}>
|
|
270
|
+
<Card size="small" style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
271
|
+
<Statistic
|
|
272
|
+
title={<span style={{color: '#666', fontSize: 13}}>累计费用</span>}
|
|
273
|
+
value={overview?.totalCost || 0}
|
|
274
|
+
prefix={<DollarOutlined style={{color: '#faad14'}}/>}
|
|
275
|
+
valueStyle={{color: '#333', fontSize: 22, fontWeight: 700}}
|
|
276
|
+
/>
|
|
277
|
+
</Card>
|
|
278
|
+
</Col>
|
|
279
|
+
<Col span={6}>
|
|
280
|
+
<Card size="small" style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
281
|
+
<Statistic
|
|
282
|
+
title={<span style={{color: '#666', fontSize: 13}}>今日 Token</span>}
|
|
283
|
+
value={overview?.todayTokens || 0}
|
|
284
|
+
prefix={<BarChartOutlined style={{color: '#52c41a'}}/>}
|
|
285
|
+
formatter={v => v?.toLocaleString()}
|
|
286
|
+
valueStyle={{color: '#333', fontSize: 22, fontWeight: 700}}
|
|
287
|
+
/>
|
|
288
|
+
</Card>
|
|
289
|
+
</Col>
|
|
290
|
+
<Col span={6}>
|
|
291
|
+
<Card size="small" style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
292
|
+
<Statistic
|
|
293
|
+
title={<span style={{color: '#666', fontSize: 13}}>今日费用</span>}
|
|
294
|
+
value={overview?.todayCost || 0}
|
|
295
|
+
prefix={<DollarOutlined style={{color: '#faad14'}}/>}
|
|
296
|
+
precision={4}
|
|
297
|
+
suffix="元"
|
|
298
|
+
valueStyle={{color: '#333', fontSize: 22, fontWeight: 700}}
|
|
299
|
+
/>
|
|
300
|
+
</Card>
|
|
301
|
+
</Col>
|
|
302
|
+
</Row>
|
|
303
|
+
|
|
304
|
+
<Tabs defaultActiveKey="trend">
|
|
305
|
+
<TabPane tab="用量趋势" key="trend">
|
|
306
|
+
<Card style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
307
|
+
<ReactECharts option={trendOption} style={{height: 300}}/>
|
|
308
|
+
</Card>
|
|
309
|
+
</TabPane>
|
|
310
|
+
|
|
311
|
+
<TabPane tab="按应用统计" key="byApp">
|
|
312
|
+
<Card style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
313
|
+
<Table
|
|
314
|
+
columns={appColumns}
|
|
315
|
+
dataSource={byApp}
|
|
316
|
+
rowKey="appCode"
|
|
317
|
+
pagination={false}
|
|
318
|
+
locale={{emptyText: '暂无数据'}}
|
|
319
|
+
/>
|
|
320
|
+
</Card>
|
|
321
|
+
</TabPane>
|
|
322
|
+
|
|
323
|
+
<TabPane tab="按用户统计" key="byUser">
|
|
324
|
+
<Card style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
325
|
+
<Table
|
|
326
|
+
columns={userColumns}
|
|
327
|
+
dataSource={byUser}
|
|
328
|
+
rowKey="userId"
|
|
329
|
+
pagination={false}
|
|
330
|
+
locale={{emptyText: '暂无数据'}}
|
|
331
|
+
/>
|
|
332
|
+
</Card>
|
|
333
|
+
</TabPane>
|
|
334
|
+
|
|
335
|
+
<TabPane tab="调用明细" key="records">
|
|
336
|
+
<Card style={{borderRadius: 10, boxShadow: '0 1px 4px rgba(0,0,0,0.06)'}}>
|
|
337
|
+
<Table
|
|
338
|
+
columns={recordColumns}
|
|
339
|
+
dataSource={records}
|
|
340
|
+
loading={loading}
|
|
341
|
+
rowKey="id"
|
|
342
|
+
pagination={{pageSize: 20, showSizeChanger: false}}
|
|
343
|
+
locale={{emptyText: '暂无数据'}}
|
|
344
|
+
/>
|
|
345
|
+
</Card>
|
|
346
|
+
</TabPane>
|
|
347
|
+
</Tabs>
|
|
348
|
+
</div>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export default UsageDashboard
|