@lobehub/lobehub 2.0.0-next.38 → 2.0.0-next.39
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/CHANGELOG.md +17 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/dispatcher.test.ts +401 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/tester.test.ts +531 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/urlBuilder.test.ts +349 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/validator.test.ts +492 -0
- package/changelog/v1.json +5 -0
- package/locales/ar/auth.json +45 -1
- package/locales/bg-BG/auth.json +45 -1
- package/locales/de-DE/auth.json +45 -1
- package/locales/en-US/auth.json +45 -1
- package/locales/es-ES/auth.json +45 -1
- package/locales/fa-IR/auth.json +45 -1
- package/locales/fr-FR/auth.json +45 -1
- package/locales/it-IT/auth.json +45 -1
- package/locales/ja-JP/auth.json +45 -1
- package/locales/ko-KR/auth.json +45 -1
- package/locales/nl-NL/auth.json +45 -1
- package/locales/pl-PL/auth.json +45 -1
- package/locales/pt-BR/auth.json +45 -1
- package/locales/ru-RU/auth.json +45 -1
- package/locales/tr-TR/auth.json +45 -1
- package/locales/vi-VN/auth.json +45 -1
- package/locales/zh-CN/auth.json +45 -1
- package/locales/zh-TW/auth.json +45 -1
- package/package.json +1 -1
- package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
- package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
- package/packages/obervability-otel/package.json +3 -1
- package/packages/obervability-otel/src/api.ts +2 -0
- package/packages/obervability-otel/src/trpc/convention.ts +16 -0
- package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
- package/packages/obervability-otel/src/trpc/index.ts +62 -0
- package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
- package/packages/types/src/usage/usageRecord.ts +54 -0
- package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
- package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
- package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
- package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
- package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
- package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
- package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
- package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
- package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
- package/src/features/Conversation/Messages/Group/index.tsx +7 -2
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
- package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
- package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
- package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
- package/src/features/PluginsUI/Render/index.tsx +17 -0
- package/src/libs/mcp/client.ts +3 -2
- package/src/libs/mcp/types.ts +71 -0
- package/src/libs/trpc/lambda/index.ts +5 -2
- package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
- package/src/locales/default/auth.ts +44 -0
- package/src/locales/default/chat.ts +1 -0
- package/src/server/routers/desktop/mcp.ts +1 -3
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/usage.ts +36 -0
- package/src/server/routers/tools/mcp.ts +1 -3
- package/src/server/services/mcp/index.test.ts +28 -15
- package/src/server/services/mcp/index.ts +29 -18
- package/src/server/services/usage/index.test.ts +310 -0
- package/src/server/services/usage/index.ts +164 -0
- package/src/services/chat/contextEngineering.test.ts +4 -0
- package/src/services/mcp.test.ts +7 -1
- package/src/services/mcp.ts +13 -12
- package/src/services/usage.ts +13 -0
- package/src/store/chat/agents/createAgentExecutors.ts +2 -3
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
- package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
- package/src/store/chat/slices/message/initialState.ts +5 -0
- package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
- package/src/store/chat/slices/message/selectors/chat.ts +0 -2
- package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
- package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
- package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
- package/src/store/chat/slices/plugin/action.test.ts +4 -4
- package/src/store/chat/slices/plugin/actions/index.ts +39 -0
- package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
- package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
- package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
- package/src/store/chat/store.ts +1 -1
- package/src/store/global/initialState.ts +1 -0
- package/src/store/chat/slices/plugin/action.ts +0 -539
package/locales/zh-CN/auth.json
CHANGED
|
@@ -145,6 +145,50 @@
|
|
|
145
145
|
"apikey": "API Key 管理",
|
|
146
146
|
"profile": "个人资料",
|
|
147
147
|
"security": "安全",
|
|
148
|
-
"stats": "数据统计"
|
|
148
|
+
"stats": "数据统计",
|
|
149
|
+
"usage": "用量统计"
|
|
150
|
+
},
|
|
151
|
+
"usage": {
|
|
152
|
+
"activeModels": {
|
|
153
|
+
"modelTable": "模型列表",
|
|
154
|
+
"models": "活跃模型",
|
|
155
|
+
"providerTable": "提供商列表",
|
|
156
|
+
"providers": "活跃提供商",
|
|
157
|
+
"table": {
|
|
158
|
+
"calls": "调用次数",
|
|
159
|
+
"model": "模型",
|
|
160
|
+
"provider": "提供商",
|
|
161
|
+
"spend": "花费"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"cards": {
|
|
165
|
+
"month": {
|
|
166
|
+
"modelCalls": "模型调用",
|
|
167
|
+
"title": "本月花费"
|
|
168
|
+
},
|
|
169
|
+
"today": {
|
|
170
|
+
"title": "今日花费",
|
|
171
|
+
"yesterday": "昨日"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"table": {
|
|
175
|
+
"actions": "操作",
|
|
176
|
+
"createdAt": "使用时间",
|
|
177
|
+
"inputTokens": "输入 Token",
|
|
178
|
+
"model": "模型",
|
|
179
|
+
"outputTokens": "输出 Token",
|
|
180
|
+
"spend": "花费",
|
|
181
|
+
"tps": "TPS",
|
|
182
|
+
"ttft": "TTFT",
|
|
183
|
+
"type": "调用类型"
|
|
184
|
+
},
|
|
185
|
+
"trends": {
|
|
186
|
+
"spend": "金额",
|
|
187
|
+
"tokens": "Token"
|
|
188
|
+
},
|
|
189
|
+
"welcome": {
|
|
190
|
+
"model": "模型",
|
|
191
|
+
"provider": "提供商"
|
|
192
|
+
}
|
|
149
193
|
}
|
|
150
194
|
}
|
package/locales/zh-TW/auth.json
CHANGED
|
@@ -145,6 +145,50 @@
|
|
|
145
145
|
"apikey": "API Key 管理",
|
|
146
146
|
"profile": "個人資料",
|
|
147
147
|
"security": "安全",
|
|
148
|
-
"stats": "數據統計"
|
|
148
|
+
"stats": "數據統計",
|
|
149
|
+
"usage": "用量統計"
|
|
150
|
+
},
|
|
151
|
+
"usage": {
|
|
152
|
+
"activeModels": {
|
|
153
|
+
"modelTable": "模型列表",
|
|
154
|
+
"models": "活躍模型",
|
|
155
|
+
"providerTable": "供應商列表",
|
|
156
|
+
"providers": "活躍供應商",
|
|
157
|
+
"table": {
|
|
158
|
+
"calls": "調用次數",
|
|
159
|
+
"model": "模型",
|
|
160
|
+
"provider": "供應商",
|
|
161
|
+
"spend": "花費"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"cards": {
|
|
165
|
+
"month": {
|
|
166
|
+
"modelCalls": "模型調用",
|
|
167
|
+
"title": "本月花費"
|
|
168
|
+
},
|
|
169
|
+
"today": {
|
|
170
|
+
"title": "今日花費",
|
|
171
|
+
"yesterday": "昨日"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"table": {
|
|
175
|
+
"actions": "操作",
|
|
176
|
+
"createdAt": "使用時間",
|
|
177
|
+
"inputTokens": "輸入 Token",
|
|
178
|
+
"model": "模型",
|
|
179
|
+
"outputTokens": "輸出 Token",
|
|
180
|
+
"spend": "花費",
|
|
181
|
+
"tps": "TPS",
|
|
182
|
+
"ttft": "TTFT",
|
|
183
|
+
"type": "調用類型"
|
|
184
|
+
},
|
|
185
|
+
"trends": {
|
|
186
|
+
"spend": "金額",
|
|
187
|
+
"tokens": "Token"
|
|
188
|
+
},
|
|
189
|
+
"welcome": {
|
|
190
|
+
"model": "模型",
|
|
191
|
+
"provider": "供應商"
|
|
192
|
+
}
|
|
149
193
|
}
|
|
150
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.39",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -131,6 +131,34 @@ describe('MessageCleanupProcessor', () => {
|
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
it('should preserve reasoning in assistant messages', async () => {
|
|
135
|
+
const processor = new MessageCleanupProcessor();
|
|
136
|
+
const reasoning = {
|
|
137
|
+
content: 'Let me think about this...',
|
|
138
|
+
signature: 'sha256:abc123',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const context = createContext([
|
|
142
|
+
{
|
|
143
|
+
content: 'Here is the answer',
|
|
144
|
+
extraField: 'remove',
|
|
145
|
+
id: 'msg5',
|
|
146
|
+
reasoning: reasoning,
|
|
147
|
+
role: 'assistant',
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const result = await processor.process(context);
|
|
153
|
+
|
|
154
|
+
expect(result.messages).toHaveLength(1);
|
|
155
|
+
expect(result.messages[0]).toEqual({
|
|
156
|
+
content: 'Here is the answer',
|
|
157
|
+
reasoning: reasoning,
|
|
158
|
+
role: 'assistant',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
134
162
|
it('should clean tool messages with name', async () => {
|
|
135
163
|
const processor = new MessageCleanupProcessor();
|
|
136
164
|
const context = createContext([
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export enum TRPCAttribute {
|
|
2
|
+
RPC_METHOD = 'rpc.method',
|
|
3
|
+
RPC_SERVICE = 'rpc.service',
|
|
4
|
+
RPC_SYSTEM = 'rpc.system',
|
|
5
|
+
RPC_TRPC_PATH = 'rpc.trpc.path',
|
|
6
|
+
RPC_TRPC_STATUS_CODE = 'rpc.trpc.status_code',
|
|
7
|
+
RPC_TRPC_SUCCESS = 'rpc.trpc.success',
|
|
8
|
+
RPC_TRPC_TYPE = 'rpc.trpc.type',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
ATTR_ERROR_TYPE,
|
|
13
|
+
ATTR_EXCEPTION_MESSAGE,
|
|
14
|
+
ATTR_EXCEPTION_STACKTRACE,
|
|
15
|
+
ATTR_EXCEPTION_TYPE,
|
|
16
|
+
} from '@opentelemetry/semantic-conventions';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseTRPCPath, tRPCConventionFromPathAndType } from './';
|
|
2
|
+
|
|
3
|
+
describe('splitRpcPath', () => {
|
|
4
|
+
it('should split path into service and method with url', () => {
|
|
5
|
+
const { method, service } = parseTRPCPath('/trpc/lambda/someService.someMethod');
|
|
6
|
+
expect(method).toBe('someMethod');
|
|
7
|
+
expect(service).toBe('someService');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should split path into service and method without url', () => {
|
|
11
|
+
const { method, service } = parseTRPCPath('someService.someMethod');
|
|
12
|
+
expect(method).toBe('someMethod');
|
|
13
|
+
expect(service).toBe('someService');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('createBaseAttributes', () => {
|
|
18
|
+
it('should create base attributes with service and method', () => {
|
|
19
|
+
const attributes = tRPCConventionFromPathAndType('someService.someMethod', 'query');
|
|
20
|
+
expect(attributes).toEqual({
|
|
21
|
+
'rpc.system': 'trpc',
|
|
22
|
+
'rpc.trpc.path': 'someService.someMethod',
|
|
23
|
+
'rpc.trpc.type': 'query',
|
|
24
|
+
'rpc.service': 'someService',
|
|
25
|
+
'rpc.method': 'someMethod',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should create base attributes without service', () => {
|
|
30
|
+
const attributes = tRPCConventionFromPathAndType('someMethod', 'mutation');
|
|
31
|
+
expect(attributes).toEqual({
|
|
32
|
+
'rpc.system': 'trpc',
|
|
33
|
+
'rpc.trpc.path': 'someMethod',
|
|
34
|
+
'rpc.trpc.type': 'mutation',
|
|
35
|
+
'rpc.method': 'someMethod',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Attributes } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
import { TRPCAttribute } from './convention';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_ERROR_CODE = 'UNKNOWN_ERROR';
|
|
6
|
+
export const DEFAULT_SUCCESS_STATUS = 'OK';
|
|
7
|
+
|
|
8
|
+
const textEncoder = new TextEncoder();
|
|
9
|
+
|
|
10
|
+
export function parseTRPCPath(path: string) {
|
|
11
|
+
const parts = path?.split('.') ?? [];
|
|
12
|
+
if (parts.length <= 1) {
|
|
13
|
+
return { method: path, service: undefined };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const method = parts.at(-1);
|
|
17
|
+
const service = parts.slice(0, -1).join('.').split('/').pop();
|
|
18
|
+
return { method, service };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function tRPCConventionFromPathAndType(path: string, type: string): Attributes {
|
|
22
|
+
const { method, service } = parseTRPCPath(path);
|
|
23
|
+
|
|
24
|
+
const attributes: Attributes = {
|
|
25
|
+
[TRPCAttribute.RPC_SYSTEM]: 'trpc',
|
|
26
|
+
[TRPCAttribute.RPC_TRPC_PATH]: path,
|
|
27
|
+
[TRPCAttribute.RPC_TRPC_TYPE]: type,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (service) {
|
|
31
|
+
attributes[TRPCAttribute.RPC_SERVICE] = service;
|
|
32
|
+
}
|
|
33
|
+
if (method) {
|
|
34
|
+
attributes[TRPCAttribute.RPC_METHOD] = method;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return attributes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const getPayloadSize = (payload: unknown): number | undefined => {
|
|
41
|
+
if (payload === undefined || payload === null) return undefined;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const serialized = JSON.stringify(payload);
|
|
45
|
+
return textEncoder.encode(serialized).length;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const createAttributesForMetrics = (
|
|
52
|
+
baseAttributes: Attributes,
|
|
53
|
+
statusCode: string,
|
|
54
|
+
extraAttributes?: Attributes,
|
|
55
|
+
): Attributes => ({
|
|
56
|
+
...baseAttributes,
|
|
57
|
+
[TRPCAttribute.RPC_TRPC_STATUS_CODE]: statusCode,
|
|
58
|
+
...extraAttributes,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export * from './convention';
|
|
62
|
+
export * from './metrics';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { metrics } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
const meter = metrics.getMeter('trpc-server');
|
|
4
|
+
|
|
5
|
+
export const serverDurationHistogram = meter.createHistogram('rpc.server.duration', {
|
|
6
|
+
description: 'Measures the duration of inbound RPC.',
|
|
7
|
+
unit: 'ms',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const serverRequestSizeHistogram = meter.createHistogram('rpc.server.request.size', {
|
|
11
|
+
description: 'Measures the size of RPC request messages (uncompressed).',
|
|
12
|
+
unit: 'By',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const serverResponseSizeHistogram = meter.createHistogram('rpc.server.response.size', {
|
|
16
|
+
description: 'Measures the size of RPC response messages (uncompressed).',
|
|
17
|
+
unit: 'By',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const serverRequestsPerRpcHistogram = meter.createHistogram('rpc.server.requests_per_rpc', {
|
|
21
|
+
description: 'Measures the number of messages received per RPC.',
|
|
22
|
+
unit: '{count}',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const serverResponsesPerRpcHistogram = meter.createHistogram(
|
|
26
|
+
'rpc.server.responses_per_rpc',
|
|
27
|
+
{
|
|
28
|
+
description: 'Measures the number of messages sent per RPC.',
|
|
29
|
+
unit: '{count}',
|
|
30
|
+
},
|
|
31
|
+
);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MessageMetadata } from '../message';
|
|
2
|
+
|
|
3
|
+
export interface UsageRecordItem {
|
|
4
|
+
createdAt: Date;
|
|
5
|
+
/**
|
|
6
|
+
* ID
|
|
7
|
+
**/
|
|
8
|
+
id: string;
|
|
9
|
+
inputStartAt?: Date | null;
|
|
10
|
+
/**
|
|
11
|
+
* Meta information
|
|
12
|
+
**/
|
|
13
|
+
metadata?: MessageMetadata | null;
|
|
14
|
+
/**
|
|
15
|
+
* Model id
|
|
16
|
+
*/
|
|
17
|
+
model: string;
|
|
18
|
+
outputFinishAt?: Date | null;
|
|
19
|
+
outputStartAt?: Date | null;
|
|
20
|
+
/**
|
|
21
|
+
* Provider id
|
|
22
|
+
*/
|
|
23
|
+
provider: string;
|
|
24
|
+
/**
|
|
25
|
+
* Spend
|
|
26
|
+
**/
|
|
27
|
+
spend: number;
|
|
28
|
+
/**
|
|
29
|
+
* Usage details
|
|
30
|
+
**/
|
|
31
|
+
totalInputTokens?: number | null;
|
|
32
|
+
totalOutputTokens?: number | null;
|
|
33
|
+
totalTokens?: number | null;
|
|
34
|
+
/**
|
|
35
|
+
* Performance details
|
|
36
|
+
**/
|
|
37
|
+
tps?: number | null;
|
|
38
|
+
ttft?: number | null;
|
|
39
|
+
/**
|
|
40
|
+
* Call types
|
|
41
|
+
**/
|
|
42
|
+
type: string;
|
|
43
|
+
updatedAt: Date;
|
|
44
|
+
userId: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type UsageLog = {
|
|
48
|
+
date: number;
|
|
49
|
+
day: string;
|
|
50
|
+
records: UsageRecordItem[];
|
|
51
|
+
totalRequests: number;
|
|
52
|
+
totalSpend: number;
|
|
53
|
+
totalTokens: number;
|
|
54
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Icon } from '@lobehub/ui';
|
|
2
|
-
import { ChartColumnBigIcon, KeyIcon, ShieldCheck, UserCircle } from 'lucide-react';
|
|
2
|
+
import { BadgeCentIcon, ChartColumnBigIcon, KeyIcon, ShieldCheck, UserCircle } from 'lucide-react';
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
|
|
@@ -54,6 +54,15 @@ export const useCategory = () => {
|
|
|
54
54
|
</Link>
|
|
55
55
|
),
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
icon: <Icon icon={BadgeCentIcon} />,
|
|
59
|
+
key: ProfileTabs.Usage,
|
|
60
|
+
label: (
|
|
61
|
+
<Link href={'/profile/usage'} onClick={(e) => e.preventDefault()}>
|
|
62
|
+
{t('tab.usage')}
|
|
63
|
+
</Link>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
57
66
|
].filter(Boolean) as MenuProps['items'];
|
|
58
67
|
|
|
59
68
|
return cateItems;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Icon, Segmented } from '@lobehub/ui';
|
|
4
|
+
import { Col, DatePicker, DatePickerProps, Row } from 'antd';
|
|
5
|
+
import dayjs from 'dayjs';
|
|
6
|
+
import { Brain, Codesandbox } from 'lucide-react';
|
|
7
|
+
import { memo, useEffect, useState } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
|
10
|
+
|
|
11
|
+
import { useClientDataSWR } from '@/libs/swr';
|
|
12
|
+
import { usageService } from '@/services/usage';
|
|
13
|
+
import { UsageLog } from '@/types/usage/usageRecord';
|
|
14
|
+
|
|
15
|
+
import Welcome from '../stats/features/Welcome';
|
|
16
|
+
import UsageCards from './features/UsageCards';
|
|
17
|
+
import UsageTable from './features/UsageTable';
|
|
18
|
+
import UsageTrends from './features/UsageTrends';
|
|
19
|
+
|
|
20
|
+
export interface UsageChartProps {
|
|
21
|
+
data?: UsageLog[];
|
|
22
|
+
dateStrings?: string;
|
|
23
|
+
groupBy?: GroupBy;
|
|
24
|
+
inShare?: boolean;
|
|
25
|
+
isLoading?: boolean;
|
|
26
|
+
mobile?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export enum GroupBy {
|
|
30
|
+
Model = 'model',
|
|
31
|
+
Provider = 'provider',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Client = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
35
|
+
const { t, i18n } = useTranslation('auth');
|
|
36
|
+
dayjs.locale(i18n.language);
|
|
37
|
+
|
|
38
|
+
const [groupBy, setGroupBy] = useState<GroupBy>(GroupBy.Model);
|
|
39
|
+
const [dateRange, setDateRange] = useState<dayjs.Dayjs>(dayjs(new Date()));
|
|
40
|
+
const [dateStrings, setDateStrings] = useState<string>();
|
|
41
|
+
|
|
42
|
+
const { data, isLoading, mutate } = useClientDataSWR('usage-stat', async () =>
|
|
43
|
+
usageService.findAndGroupByDay(dateStrings),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (dateStrings) {
|
|
48
|
+
mutate();
|
|
49
|
+
}
|
|
50
|
+
}, [dateStrings]);
|
|
51
|
+
|
|
52
|
+
const handleDateChange: DatePickerProps['onChange'] = (dates, dateStrings) => {
|
|
53
|
+
setDateRange(dates);
|
|
54
|
+
if (typeof dateStrings === 'string') {
|
|
55
|
+
setDateStrings(dateStrings);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Flexbox gap={mobile ? 0 : 24}>
|
|
61
|
+
<Flexbox>
|
|
62
|
+
<Row>
|
|
63
|
+
<Col span={16}>
|
|
64
|
+
{mobile ? (
|
|
65
|
+
<Welcome mobile />
|
|
66
|
+
) : (
|
|
67
|
+
<Flexbox align={'flex-start'} gap={16} horizontal justify={'space-between'}>
|
|
68
|
+
<Welcome />
|
|
69
|
+
</Flexbox>
|
|
70
|
+
)}
|
|
71
|
+
</Col>
|
|
72
|
+
<Col span={8}>
|
|
73
|
+
<Flexbox gap={16} horizontal>
|
|
74
|
+
<Segmented
|
|
75
|
+
onChange={(v) => setGroupBy(v as GroupBy)}
|
|
76
|
+
options={[
|
|
77
|
+
{
|
|
78
|
+
icon: <Icon icon={Codesandbox} />,
|
|
79
|
+
label: t('usage.welcome.model'),
|
|
80
|
+
value: GroupBy.Model,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
icon: <Icon icon={Brain} />,
|
|
84
|
+
label: t('usage.welcome.provider'),
|
|
85
|
+
value: GroupBy.Provider,
|
|
86
|
+
},
|
|
87
|
+
]}
|
|
88
|
+
value={groupBy}
|
|
89
|
+
/>
|
|
90
|
+
<DatePicker onChange={handleDateChange} picker="month" value={dateRange} />
|
|
91
|
+
</Flexbox>
|
|
92
|
+
</Col>
|
|
93
|
+
</Row>
|
|
94
|
+
</Flexbox>
|
|
95
|
+
<Flexbox>
|
|
96
|
+
<UsageCards data={data} groupBy={groupBy} isLoading={isLoading} />
|
|
97
|
+
</Flexbox>
|
|
98
|
+
<Flexbox>
|
|
99
|
+
<Row gutter={[16, 16]}>
|
|
100
|
+
<Col span={24}>
|
|
101
|
+
<UsageTrends data={data} groupBy={groupBy} isLoading={isLoading} />
|
|
102
|
+
</Col>
|
|
103
|
+
</Row>
|
|
104
|
+
</Flexbox>
|
|
105
|
+
<Row>
|
|
106
|
+
<Col span={24}>
|
|
107
|
+
<UsageTable dateStrings={dateStrings} />
|
|
108
|
+
</Col>
|
|
109
|
+
</Row>
|
|
110
|
+
</Flexbox>
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export default Client;
|
package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { CategoryBar, useThemeColorRange } from '@lobehub/charts';
|
|
2
|
+
import { ModelIcon, ProviderIcon } from '@lobehub/icons';
|
|
3
|
+
import { Collapse, Tag } from '@lobehub/ui';
|
|
4
|
+
import { Skeleton } from 'antd';
|
|
5
|
+
import { useTheme } from 'antd-style';
|
|
6
|
+
import { memo, useMemo } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Flexbox } from 'react-layout-kit';
|
|
9
|
+
|
|
10
|
+
import InlineTable from '@/components/InlineTable';
|
|
11
|
+
import { UsageLog } from '@/types/usage/usageRecord';
|
|
12
|
+
import { formatPrice } from '@/utils/format';
|
|
13
|
+
|
|
14
|
+
import { GroupBy, UsageChartProps } from '../../../Client';
|
|
15
|
+
|
|
16
|
+
interface WeightGroup {
|
|
17
|
+
id: string;
|
|
18
|
+
spend: number | string;
|
|
19
|
+
weight: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatData = (
|
|
23
|
+
data: UsageLog[],
|
|
24
|
+
groupBy: GroupBy,
|
|
25
|
+
): {
|
|
26
|
+
childrens: WeightGroup[];
|
|
27
|
+
id: string;
|
|
28
|
+
totalSpend: number;
|
|
29
|
+
}[] => {
|
|
30
|
+
if (!data || data?.length === 0) return [];
|
|
31
|
+
|
|
32
|
+
const requestLogs = data.flatMap((log) => log.records);
|
|
33
|
+
const groupedLogs = requestLogs.reduce((acc, log) => {
|
|
34
|
+
const key = groupBy === GroupBy.Model ? log.model : log.provider;
|
|
35
|
+
if (!acc.has(key)) {
|
|
36
|
+
acc.set(key, []);
|
|
37
|
+
}
|
|
38
|
+
acc.get(key)?.push(log);
|
|
39
|
+
return acc;
|
|
40
|
+
}, new Map<string, UsageLog['records']>());
|
|
41
|
+
|
|
42
|
+
return Array.from(groupedLogs.entries())
|
|
43
|
+
.map(([key, logs]) => {
|
|
44
|
+
// 此处的 logs 为多日的 log,需要进行 sum
|
|
45
|
+
// 如果当前的 groupBy 是 Model,则 logs 应该按照 Provider 进行分组
|
|
46
|
+
const spend = logs.reduce((acc, log) => {
|
|
47
|
+
const key = groupBy === GroupBy.Model ? log.provider : log.model;
|
|
48
|
+
acc.set(key, (acc.get(key) || 0) + log.spend);
|
|
49
|
+
return acc;
|
|
50
|
+
}, new Map<string, number>());
|
|
51
|
+
|
|
52
|
+
const totalSpend = logs.reduce((total, log) => total + (log.spend || 0), 0);
|
|
53
|
+
|
|
54
|
+
const spendWithWeight = Array.from(
|
|
55
|
+
spend.entries().map(([key, value]) => {
|
|
56
|
+
return {
|
|
57
|
+
id: key,
|
|
58
|
+
spend: value,
|
|
59
|
+
weight: totalSpend > 0 ? value / totalSpend : 0,
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
childrens: spendWithWeight.sort((a, b) => b.weight - a.weight),
|
|
66
|
+
id: key,
|
|
67
|
+
totalSpend: totalSpend,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.totalSpend - a.totalSpend);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const ModelTable = memo<UsageChartProps>(({ data, isLoading, groupBy }) => {
|
|
74
|
+
const { t } = useTranslation('auth');
|
|
75
|
+
const theme = useTheme();
|
|
76
|
+
const themeColorRange = useThemeColorRange();
|
|
77
|
+
|
|
78
|
+
const formattedData = useMemo(
|
|
79
|
+
() => formatData(data || [], groupBy || GroupBy.Model),
|
|
80
|
+
[data, groupBy],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
console.log('ModelTable', groupBy, formattedData);
|
|
84
|
+
|
|
85
|
+
return isLoading ? (
|
|
86
|
+
<Skeleton active paragraph={{ rows: 8 }} title={false} />
|
|
87
|
+
) : (
|
|
88
|
+
<Collapse
|
|
89
|
+
defaultActiveKey={formattedData.map((item) => item.id)}
|
|
90
|
+
expandIconPosition={'end'}
|
|
91
|
+
gap={16}
|
|
92
|
+
items={formattedData.map((item) => {
|
|
93
|
+
const key = item.id;
|
|
94
|
+
return {
|
|
95
|
+
children: (
|
|
96
|
+
<Flexbox>
|
|
97
|
+
<CategoryBar
|
|
98
|
+
colors={themeColorRange}
|
|
99
|
+
showLabels={false}
|
|
100
|
+
size={2}
|
|
101
|
+
values={item.childrens.map((item) => item.weight)}
|
|
102
|
+
/>
|
|
103
|
+
<InlineTable
|
|
104
|
+
columns={[
|
|
105
|
+
{
|
|
106
|
+
dataIndex: 'id',
|
|
107
|
+
key: 'id',
|
|
108
|
+
render: (value, record, index) => {
|
|
109
|
+
return (
|
|
110
|
+
<Flexbox align={'center'} gap={12} horizontal key={value}>
|
|
111
|
+
{groupBy === GroupBy.Provider ? (
|
|
112
|
+
<ProviderIcon
|
|
113
|
+
provider={record.id}
|
|
114
|
+
style={{
|
|
115
|
+
boxShadow: `0 0 0 2px ${theme.colorBgContainer}, 0 0 0 4px ${themeColorRange[index]}`,
|
|
116
|
+
boxSizing: 'content-box',
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<ModelIcon
|
|
121
|
+
model={record.id}
|
|
122
|
+
style={{
|
|
123
|
+
boxShadow: `0 0 0 2px ${theme.colorBgContainer}, 0 0 0 4px ${themeColorRange[index]}`,
|
|
124
|
+
boxSizing: 'content-box',
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
{value}
|
|
129
|
+
</Flexbox>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
title:
|
|
133
|
+
groupBy === GroupBy.Model
|
|
134
|
+
? t('usage.activeModels.table.provider')
|
|
135
|
+
: t('usage.activeModels.table.model'),
|
|
136
|
+
width: 200,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
dataIndex: 'spend',
|
|
140
|
+
key: 'spend',
|
|
141
|
+
render: (value) => {
|
|
142
|
+
return `$${formatPrice(value)}`;
|
|
143
|
+
},
|
|
144
|
+
title: t('usage.activeModels.table.spend'),
|
|
145
|
+
},
|
|
146
|
+
]}
|
|
147
|
+
dataSource={item.childrens}
|
|
148
|
+
hoverToActive={false}
|
|
149
|
+
loading={isLoading}
|
|
150
|
+
rowKey={(record) => record.id}
|
|
151
|
+
/>
|
|
152
|
+
</Flexbox>
|
|
153
|
+
),
|
|
154
|
+
extra: <Tag>{item?.childrens?.length ?? 0}</Tag>,
|
|
155
|
+
key,
|
|
156
|
+
label: (
|
|
157
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
|
158
|
+
{groupBy === GroupBy.Model ? (
|
|
159
|
+
<ModelIcon model={key} size={24} />
|
|
160
|
+
) : (
|
|
161
|
+
<ProviderIcon provider={key} size={24} />
|
|
162
|
+
)}
|
|
163
|
+
{key}
|
|
164
|
+
</Flexbox>
|
|
165
|
+
),
|
|
166
|
+
};
|
|
167
|
+
})}
|
|
168
|
+
padding={{
|
|
169
|
+
body: 0,
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export default ModelTable;
|