@lobehub/lobehub 2.0.0-next.37 → 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 +50 -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 +14 -0
- package/locales/ar/auth.json +45 -1
- package/locales/ar/modelProvider.json +13 -1
- package/locales/bg-BG/auth.json +45 -1
- package/locales/bg-BG/modelProvider.json +13 -1
- package/locales/de-DE/auth.json +45 -1
- package/locales/de-DE/modelProvider.json +13 -1
- package/locales/en-US/auth.json +45 -1
- package/locales/en-US/modelProvider.json +13 -1
- package/locales/es-ES/auth.json +45 -1
- package/locales/es-ES/modelProvider.json +13 -1
- package/locales/fa-IR/auth.json +45 -1
- package/locales/fa-IR/modelProvider.json +13 -1
- package/locales/fr-FR/auth.json +45 -1
- package/locales/fr-FR/modelProvider.json +13 -1
- package/locales/it-IT/auth.json +45 -1
- package/locales/it-IT/modelProvider.json +13 -1
- package/locales/ja-JP/auth.json +45 -1
- package/locales/ja-JP/modelProvider.json +13 -1
- package/locales/ko-KR/auth.json +45 -1
- package/locales/ko-KR/modelProvider.json +13 -1
- package/locales/nl-NL/auth.json +45 -1
- package/locales/nl-NL/modelProvider.json +13 -1
- package/locales/pl-PL/auth.json +45 -1
- package/locales/pl-PL/modelProvider.json +13 -1
- package/locales/pt-BR/auth.json +45 -1
- package/locales/pt-BR/modelProvider.json +13 -1
- package/locales/ru-RU/auth.json +45 -1
- package/locales/ru-RU/modelProvider.json +13 -1
- package/locales/tr-TR/auth.json +45 -1
- package/locales/tr-TR/modelProvider.json +13 -1
- package/locales/vi-VN/auth.json +45 -1
- package/locales/vi-VN/modelProvider.json +13 -1
- package/locales/zh-CN/auth.json +45 -1
- package/locales/zh-CN/modelProvider.json +13 -1
- package/locales/zh-TW/auth.json +45 -1
- package/locales/zh-TW/modelProvider.json +13 -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/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
- package/packages/web-crawler/src/crawImpl/naive.ts +9 -9
- package/packages/web-crawler/src/crawler.ts +5 -5
- package/packages/web-crawler/src/urlRules.ts +13 -13
- package/packages/web-crawler/src/utils/appUrlRules.ts +5 -5
- 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/components/Extras/Usage/UsageDetail/index.tsx +3 -0
- 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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { LobeChatDatabase } from '@/database/type';
|
|
5
|
+
import { MessageMetadata } from '@/types/message';
|
|
6
|
+
|
|
7
|
+
import { UsageRecordService } from './index';
|
|
8
|
+
|
|
9
|
+
describe('UsageRecordService', () => {
|
|
10
|
+
let service: UsageRecordService;
|
|
11
|
+
let mockDb: LobeChatDatabase;
|
|
12
|
+
const userId = 'test-user-id';
|
|
13
|
+
|
|
14
|
+
// Helper function to setup query chain mock
|
|
15
|
+
const setupQueryChainMock = (mockMessages: any[]) => {
|
|
16
|
+
const mockOrderBy = vi.fn().mockResolvedValue(mockMessages);
|
|
17
|
+
const mockWhere = vi.fn().mockReturnValue({ orderBy: mockOrderBy });
|
|
18
|
+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
|
|
19
|
+
mockDb.select = vi.fn().mockReturnValue({ from: mockFrom });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Create a fresh mock for each test
|
|
24
|
+
const mockOrderBy = vi.fn();
|
|
25
|
+
const mockWhere = vi.fn().mockReturnValue({ orderBy: mockOrderBy });
|
|
26
|
+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
|
|
27
|
+
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
|
|
28
|
+
|
|
29
|
+
mockDb = {
|
|
30
|
+
select: mockSelect,
|
|
31
|
+
} as unknown as LobeChatDatabase;
|
|
32
|
+
|
|
33
|
+
service = new UsageRecordService(mockDb, userId);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('findByMonth', () => {
|
|
37
|
+
it('should return usage records for the current month when no month is provided', async () => {
|
|
38
|
+
const mockMessages = [
|
|
39
|
+
{
|
|
40
|
+
id: 'msg-1',
|
|
41
|
+
userId: userId,
|
|
42
|
+
role: 'assistant',
|
|
43
|
+
provider: 'openai',
|
|
44
|
+
model: 'gpt-4',
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
metadata: {
|
|
47
|
+
cost: 0.05,
|
|
48
|
+
totalInputTokens: 100,
|
|
49
|
+
totalOutputTokens: 50,
|
|
50
|
+
tps: 10,
|
|
51
|
+
ttft: 500,
|
|
52
|
+
} as MessageMetadata,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
setupQueryChainMock(mockMessages);
|
|
57
|
+
|
|
58
|
+
const result = await service.findByMonth();
|
|
59
|
+
|
|
60
|
+
expect(result).toHaveLength(1);
|
|
61
|
+
expect(result[0]).toMatchObject({
|
|
62
|
+
id: 'msg-1',
|
|
63
|
+
model: 'gpt-4',
|
|
64
|
+
provider: 'openai',
|
|
65
|
+
spend: 0.05,
|
|
66
|
+
totalInputTokens: 100,
|
|
67
|
+
totalOutputTokens: 50,
|
|
68
|
+
totalTokens: 150,
|
|
69
|
+
tps: 10,
|
|
70
|
+
ttft: 500,
|
|
71
|
+
type: 'chat',
|
|
72
|
+
userId: userId,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return usage records for a specific month', async () => {
|
|
77
|
+
const mockMessages = [
|
|
78
|
+
{
|
|
79
|
+
id: 'msg-1',
|
|
80
|
+
userId: userId,
|
|
81
|
+
role: 'assistant',
|
|
82
|
+
provider: 'anthropic',
|
|
83
|
+
model: 'claude-3',
|
|
84
|
+
createdAt: new Date('2024-01-15'),
|
|
85
|
+
metadata: {
|
|
86
|
+
cost: 0.03,
|
|
87
|
+
totalInputTokens: 80,
|
|
88
|
+
totalOutputTokens: 40,
|
|
89
|
+
} as MessageMetadata,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
setupQueryChainMock(mockMessages);
|
|
94
|
+
|
|
95
|
+
const result = await service.findByMonth('2024-01');
|
|
96
|
+
|
|
97
|
+
expect(result[0].model).toBe('claude-3');
|
|
98
|
+
expect(result[0].spend).toBe(0.03);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle messages with missing metadata fields', async () => {
|
|
102
|
+
const mockMessages = [
|
|
103
|
+
{
|
|
104
|
+
id: 'msg-1',
|
|
105
|
+
userId: userId,
|
|
106
|
+
role: 'assistant',
|
|
107
|
+
provider: 'openai',
|
|
108
|
+
model: 'gpt-3.5-turbo',
|
|
109
|
+
createdAt: new Date(),
|
|
110
|
+
metadata: {} as MessageMetadata,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
setupQueryChainMock(mockMessages);
|
|
115
|
+
|
|
116
|
+
const result = await service.findByMonth();
|
|
117
|
+
|
|
118
|
+
expect(result).toHaveLength(1);
|
|
119
|
+
expect(result[0]).toMatchObject({
|
|
120
|
+
spend: 0,
|
|
121
|
+
totalInputTokens: 0,
|
|
122
|
+
totalOutputTokens: 0,
|
|
123
|
+
totalTokens: 0,
|
|
124
|
+
tps: 0,
|
|
125
|
+
ttft: 0,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return empty array when no messages found', async () => {
|
|
130
|
+
setupQueryChainMock([]);
|
|
131
|
+
|
|
132
|
+
const result = await service.findByMonth();
|
|
133
|
+
|
|
134
|
+
expect(result).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('findAndGroupByDay', () => {
|
|
139
|
+
it('should group usage records by day for current month', async () => {
|
|
140
|
+
const date1 = dayjs().startOf('month').toDate();
|
|
141
|
+
const date2 = dayjs().startOf('month').add(1, 'day').toDate();
|
|
142
|
+
|
|
143
|
+
const mockMessages = [
|
|
144
|
+
{
|
|
145
|
+
id: 'msg-1',
|
|
146
|
+
userId: userId,
|
|
147
|
+
role: 'assistant',
|
|
148
|
+
provider: 'openai',
|
|
149
|
+
model: 'gpt-4',
|
|
150
|
+
createdAt: date1,
|
|
151
|
+
metadata: {
|
|
152
|
+
cost: 0.05,
|
|
153
|
+
totalInputTokens: 100,
|
|
154
|
+
totalOutputTokens: 50,
|
|
155
|
+
} as MessageMetadata,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'msg-2',
|
|
159
|
+
userId: userId,
|
|
160
|
+
role: 'assistant',
|
|
161
|
+
provider: 'openai',
|
|
162
|
+
model: 'gpt-4',
|
|
163
|
+
createdAt: date1,
|
|
164
|
+
metadata: {
|
|
165
|
+
cost: 0.03,
|
|
166
|
+
totalInputTokens: 60,
|
|
167
|
+
totalOutputTokens: 30,
|
|
168
|
+
} as MessageMetadata,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'msg-3',
|
|
172
|
+
userId: userId,
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
provider: 'anthropic',
|
|
175
|
+
model: 'claude-3',
|
|
176
|
+
createdAt: date2,
|
|
177
|
+
metadata: {
|
|
178
|
+
cost: 0.02,
|
|
179
|
+
totalInputTokens: 40,
|
|
180
|
+
totalOutputTokens: 20,
|
|
181
|
+
} as MessageMetadata,
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
setupQueryChainMock(mockMessages);
|
|
186
|
+
|
|
187
|
+
const result = await service.findAndGroupByDay();
|
|
188
|
+
|
|
189
|
+
expect(result.length).toBeGreaterThan(0);
|
|
190
|
+
|
|
191
|
+
// Check that days with records have correct aggregations
|
|
192
|
+
const dayWithRecords = result.find((log) => log.totalRequests > 0);
|
|
193
|
+
if (dayWithRecords) {
|
|
194
|
+
expect(dayWithRecords.totalSpend).toBeGreaterThan(0);
|
|
195
|
+
expect(dayWithRecords.totalTokens).toBeGreaterThan(0);
|
|
196
|
+
expect(dayWithRecords.records.length).toBeGreaterThan(0);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should pad missing days with zero values', async () => {
|
|
201
|
+
const firstDay = dayjs().startOf('month');
|
|
202
|
+
|
|
203
|
+
const mockMessages = [
|
|
204
|
+
{
|
|
205
|
+
id: 'msg-1',
|
|
206
|
+
userId: userId,
|
|
207
|
+
role: 'assistant',
|
|
208
|
+
provider: 'openai',
|
|
209
|
+
model: 'gpt-4',
|
|
210
|
+
createdAt: firstDay.toDate(),
|
|
211
|
+
metadata: {
|
|
212
|
+
cost: 0.05,
|
|
213
|
+
totalInputTokens: 100,
|
|
214
|
+
totalOutputTokens: 50,
|
|
215
|
+
} as MessageMetadata,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
setupQueryChainMock(mockMessages);
|
|
220
|
+
|
|
221
|
+
const result = await service.findAndGroupByDay();
|
|
222
|
+
|
|
223
|
+
// Should have entries for every day in the month
|
|
224
|
+
const daysInMonth = dayjs().endOf('month').date();
|
|
225
|
+
expect(result.length).toBeGreaterThanOrEqual(daysInMonth - 1);
|
|
226
|
+
|
|
227
|
+
// Check that padded days have zero values
|
|
228
|
+
const paddedDay = result.find((log) => log.totalRequests === 0);
|
|
229
|
+
if (paddedDay) {
|
|
230
|
+
expect(paddedDay.totalSpend).toBe(0);
|
|
231
|
+
expect(paddedDay.totalTokens).toBe(0);
|
|
232
|
+
expect(paddedDay.records).toHaveLength(0);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should calculate correct totals for days with multiple records', async () => {
|
|
237
|
+
const testDate = dayjs().startOf('month').toDate();
|
|
238
|
+
|
|
239
|
+
const mockMessages = [
|
|
240
|
+
{
|
|
241
|
+
id: 'msg-1',
|
|
242
|
+
userId: userId,
|
|
243
|
+
role: 'assistant',
|
|
244
|
+
provider: 'openai',
|
|
245
|
+
model: 'gpt-4',
|
|
246
|
+
createdAt: testDate,
|
|
247
|
+
metadata: {
|
|
248
|
+
cost: 0.05,
|
|
249
|
+
totalInputTokens: 100,
|
|
250
|
+
totalOutputTokens: 50,
|
|
251
|
+
} as MessageMetadata,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'msg-2',
|
|
255
|
+
userId: userId,
|
|
256
|
+
role: 'assistant',
|
|
257
|
+
provider: 'openai',
|
|
258
|
+
model: 'gpt-4',
|
|
259
|
+
createdAt: testDate,
|
|
260
|
+
metadata: {
|
|
261
|
+
cost: 0.03,
|
|
262
|
+
totalInputTokens: 60,
|
|
263
|
+
totalOutputTokens: 30,
|
|
264
|
+
} as MessageMetadata,
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
setupQueryChainMock(mockMessages);
|
|
269
|
+
|
|
270
|
+
const result = await service.findAndGroupByDay();
|
|
271
|
+
|
|
272
|
+
const dayLog = result.find((log) => log.totalRequests === 2);
|
|
273
|
+
|
|
274
|
+
if (dayLog) {
|
|
275
|
+
expect(dayLog.totalSpend).toBe(0.08);
|
|
276
|
+
expect(dayLog.totalTokens).toBe(240); // (100+50) + (60+30)
|
|
277
|
+
expect(dayLog.totalRequests).toBe(2);
|
|
278
|
+
expect(dayLog.records).toHaveLength(2);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle specific month parameter', async () => {
|
|
283
|
+
const mockMessages = [
|
|
284
|
+
{
|
|
285
|
+
id: 'msg-1',
|
|
286
|
+
userId: userId,
|
|
287
|
+
role: 'assistant',
|
|
288
|
+
provider: 'openai',
|
|
289
|
+
model: 'gpt-4',
|
|
290
|
+
createdAt: new Date('2024-01-15'),
|
|
291
|
+
metadata: {
|
|
292
|
+
cost: 0.05,
|
|
293
|
+
totalInputTokens: 100,
|
|
294
|
+
totalOutputTokens: 50,
|
|
295
|
+
} as MessageMetadata,
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
setupQueryChainMock(mockMessages);
|
|
300
|
+
|
|
301
|
+
const result = await service.findAndGroupByDay('2024-01');
|
|
302
|
+
|
|
303
|
+
expect(result.length).toBeGreaterThan(0);
|
|
304
|
+
// All days should be from January 2024
|
|
305
|
+
result.forEach((log) => {
|
|
306
|
+
expect(log.day).toMatch(/^2024-01/);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { desc, eq } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
import { messages } from '@/database/schemas';
|
|
6
|
+
import { LobeChatDatabase } from '@/database/type';
|
|
7
|
+
import { genRangeWhere, genWhere } from '@/database/utils/genWhere';
|
|
8
|
+
import { MessageMetadata } from '@/types/message';
|
|
9
|
+
import { UsageLog, UsageRecordItem } from '@/types/usage/usageRecord';
|
|
10
|
+
import { formatDate } from '@/utils/format';
|
|
11
|
+
|
|
12
|
+
const log = debug('lobe-usage:service');
|
|
13
|
+
|
|
14
|
+
export class UsageRecordService {
|
|
15
|
+
private userId: string;
|
|
16
|
+
private db: LobeChatDatabase;
|
|
17
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
|
18
|
+
this.userId = userId;
|
|
19
|
+
this.db = db;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @description Find usage records by month.
|
|
24
|
+
* @param mo Month
|
|
25
|
+
* @returns UsageRecordItem[]
|
|
26
|
+
*/
|
|
27
|
+
findByMonth = async (mo?: string): Promise<UsageRecordItem[]> => {
|
|
28
|
+
// 设置 startAt 和 endAt
|
|
29
|
+
let startAt: string;
|
|
30
|
+
let endAt: string;
|
|
31
|
+
if (mo) {
|
|
32
|
+
// mo 格式: "YYYY-MM"
|
|
33
|
+
startAt = dayjs(mo, 'YYYY-MM').startOf('month').format('YYYY-MM-DD');
|
|
34
|
+
endAt = dayjs(mo, 'YYYY-MM').endOf('month').format('YYYY-MM-DD');
|
|
35
|
+
} else {
|
|
36
|
+
startAt = dayjs().startOf('month').format('YYYY-MM-DD');
|
|
37
|
+
endAt = dayjs().endOf('month').format('YYYY-MM-DD');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// TODO: To extend to support other features
|
|
41
|
+
// - Functionality:
|
|
42
|
+
// - More type of usage, e.g. image generation, file processing, summary, search engine.
|
|
43
|
+
// - More dimension for analysis, e.g. relational analysis.
|
|
44
|
+
// - Performance: Computing asynchronously for performance.
|
|
45
|
+
// For now, we only support chat messages for normal users for PoC.
|
|
46
|
+
|
|
47
|
+
const spends = await this.db
|
|
48
|
+
.select({
|
|
49
|
+
createdAt: messages.createdAt,
|
|
50
|
+
id: messages.id,
|
|
51
|
+
metadata: messages.metadata,
|
|
52
|
+
model: messages.model,
|
|
53
|
+
provider: messages.provider,
|
|
54
|
+
role: messages.role,
|
|
55
|
+
updatedAt: messages.createdAt,
|
|
56
|
+
userId: messages.userId,
|
|
57
|
+
})
|
|
58
|
+
.from(messages)
|
|
59
|
+
.where(
|
|
60
|
+
genWhere([
|
|
61
|
+
eq(messages.userId, this.userId),
|
|
62
|
+
eq(messages.role, 'assistant'),
|
|
63
|
+
genRangeWhere([startAt, endAt], messages.createdAt, (date) => date.toDate()),
|
|
64
|
+
]),
|
|
65
|
+
)
|
|
66
|
+
.orderBy(desc(messages.createdAt));
|
|
67
|
+
return spends.map((spend) => {
|
|
68
|
+
const metadata = spend.metadata as MessageMetadata;
|
|
69
|
+
return {
|
|
70
|
+
createdAt: spend.createdAt,
|
|
71
|
+
id: spend.id,
|
|
72
|
+
metadata: spend.metadata,
|
|
73
|
+
model: spend.model,
|
|
74
|
+
provider: spend.provider,
|
|
75
|
+
spend: metadata?.cost || 0, // Messages do not have a direct cost associated
|
|
76
|
+
totalInputTokens: metadata?.totalInputTokens || 0,
|
|
77
|
+
totalOutputTokens: metadata?.totalOutputTokens || 0,
|
|
78
|
+
totalTokens: (metadata?.totalInputTokens || 0) + (metadata?.totalOutputTokens || 0),
|
|
79
|
+
tps: metadata?.tps || 0,
|
|
80
|
+
ttft: metadata?.ttft || 0,
|
|
81
|
+
type: 'chat', // Default to 'chat' for messages
|
|
82
|
+
updatedAt: spend.createdAt,
|
|
83
|
+
userId: spend.userId,
|
|
84
|
+
} as UsageRecordItem;
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
findAndGroupByDay = async (mo?: string): Promise<UsageLog[]> => {
|
|
89
|
+
// 设置 startAt 和 endAt
|
|
90
|
+
let startAt: string;
|
|
91
|
+
let endAt: string;
|
|
92
|
+
if (mo) {
|
|
93
|
+
// mo 格式: "YYYY-MM"
|
|
94
|
+
startAt = dayjs(mo, 'YYYY-MM').startOf('month').format('YYYY-MM-DD');
|
|
95
|
+
endAt = dayjs(mo, 'YYYY-MM').endOf('month').format('YYYY-MM-DD');
|
|
96
|
+
} else {
|
|
97
|
+
startAt = dayjs().startOf('month').format('YYYY-MM-DD');
|
|
98
|
+
endAt = dayjs().endOf('month').format('YYYY-MM-DD');
|
|
99
|
+
}
|
|
100
|
+
const spends = await this.findByMonth(mo);
|
|
101
|
+
// Clustering by time
|
|
102
|
+
let usages = new Map<string, { date: Date; logs: UsageRecordItem[] }>();
|
|
103
|
+
spends.forEach((spend) => {
|
|
104
|
+
if (!usages.has(formatDate(spend.createdAt))) {
|
|
105
|
+
usages.set(formatDate(spend.createdAt), { date: spend.createdAt, logs: [spend] });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
usages.get(formatDate(spend.createdAt))?.logs.push(spend);
|
|
109
|
+
});
|
|
110
|
+
// Calculate usage
|
|
111
|
+
let usageLogs: UsageLog[] = [];
|
|
112
|
+
usages.forEach((spends, date) => {
|
|
113
|
+
const totalSpend = spends.logs.reduce((acc, spend) => acc + spend.spend, 0);
|
|
114
|
+
const totalTokens = spends.logs.reduce((acc, spend) => (spend.totalTokens || 0) + acc, 0);
|
|
115
|
+
const totalRequests = spends.logs?.length ?? 0;
|
|
116
|
+
log(
|
|
117
|
+
'date',
|
|
118
|
+
date,
|
|
119
|
+
'totalSpend',
|
|
120
|
+
totalSpend,
|
|
121
|
+
'totalTokens',
|
|
122
|
+
totalTokens,
|
|
123
|
+
'totalRequests',
|
|
124
|
+
totalRequests,
|
|
125
|
+
);
|
|
126
|
+
usageLogs.push({
|
|
127
|
+
date: spends.date.getTime(),
|
|
128
|
+
day: date,
|
|
129
|
+
records: spends.logs,
|
|
130
|
+
totalRequests,
|
|
131
|
+
totalSpend,
|
|
132
|
+
totalTokens, // Store the formatted date as a string
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// Padding to ensure the date range is complete
|
|
136
|
+
const startDate = dayjs(startAt);
|
|
137
|
+
const endDate = dayjs(endAt);
|
|
138
|
+
const paddedUsageLogs: UsageLog[] = [];
|
|
139
|
+
// For every day in the range, check if it exists in usageLogs
|
|
140
|
+
// If exists, use it; if not, create a new log with 0 values
|
|
141
|
+
log(
|
|
142
|
+
'Padding usage logs from',
|
|
143
|
+
startDate.format('YYYY-MM-DD'),
|
|
144
|
+
'to',
|
|
145
|
+
endDate.format('YYYY-MM-DD'),
|
|
146
|
+
);
|
|
147
|
+
for (let date = startDate; date.isBefore(endDate); date = date.add(1, 'day')) {
|
|
148
|
+
const log = usageLogs.find((log) => log.day === date.format('YYYY-MM-DD'));
|
|
149
|
+
if (log) {
|
|
150
|
+
paddedUsageLogs.push(log);
|
|
151
|
+
} else {
|
|
152
|
+
paddedUsageLogs.push({
|
|
153
|
+
date: date.toDate().getTime(),
|
|
154
|
+
day: date.format('YYYY-MM-DD'),
|
|
155
|
+
records: [],
|
|
156
|
+
totalRequests: 0,
|
|
157
|
+
totalSpend: 0,
|
|
158
|
+
totalTokens: 0,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return paddedUsageLogs;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -243,6 +243,10 @@ describe('contextEngineering', () => {
|
|
|
243
243
|
type: 'text',
|
|
244
244
|
},
|
|
245
245
|
],
|
|
246
|
+
reasoning: {
|
|
247
|
+
content: 'I need to calculate the answer to life, universe, and everything.',
|
|
248
|
+
signature: 'thinking_process',
|
|
249
|
+
},
|
|
246
250
|
role: 'assistant',
|
|
247
251
|
},
|
|
248
252
|
]);
|
package/src/services/mcp.test.ts
CHANGED
|
@@ -305,7 +305,13 @@ describe('MCPService', () => {
|
|
|
305
305
|
mockPluginSelectors.getInstalledPluginById.mockReturnValue(() => mockPlugin);
|
|
306
306
|
mockPluginSelectors.getCustomPluginById.mockReturnValue(() => null);
|
|
307
307
|
|
|
308
|
-
const mockResult =
|
|
308
|
+
const mockResult = {
|
|
309
|
+
content: 'response data',
|
|
310
|
+
state: {
|
|
311
|
+
content: [{ text: 'response data', type: 'text' }],
|
|
312
|
+
},
|
|
313
|
+
success: true,
|
|
314
|
+
};
|
|
309
315
|
vi.mocked(toolsClient.mcp.callTool.mutate).mockResolvedValue(mockResult);
|
|
310
316
|
|
|
311
317
|
const payload: ChatToolPayload = {
|
package/src/services/mcp.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { isLocalOrPrivateUrl, safeParseJSON } from '@lobechat/utils';
|
|
|
4
4
|
import { PluginManifest } from '@lobehub/market-sdk';
|
|
5
5
|
import { CallReportRequest } from '@lobehub/market-types';
|
|
6
6
|
|
|
7
|
+
import { MCPToolCallResult } from '@/libs/mcp';
|
|
7
8
|
import { desktopClient, toolsClient } from '@/libs/trpc/client';
|
|
8
9
|
|
|
9
10
|
import { discoverService } from './discover';
|
|
@@ -44,16 +45,16 @@ class MCPService {
|
|
|
44
45
|
const connection = plugin.customParams?.mcp;
|
|
45
46
|
const settingsEntries = plugin.settings
|
|
46
47
|
? Object.entries(plugin.settings as Record<string, any>).filter(
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
([, value]) => value !== undefined && value !== null,
|
|
49
|
+
)
|
|
49
50
|
: [];
|
|
50
51
|
const pluginSettings =
|
|
51
52
|
settingsEntries.length > 0
|
|
52
53
|
? settingsEntries.reduce<Record<string, unknown>>((acc, [key, value]) => {
|
|
53
|
-
|
|
54
|
+
acc[key] = value;
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
return acc;
|
|
57
|
+
}, {})
|
|
57
58
|
: undefined;
|
|
58
59
|
|
|
59
60
|
const params = {
|
|
@@ -77,7 +78,7 @@ class MCPService {
|
|
|
77
78
|
|
|
78
79
|
const data = {
|
|
79
80
|
args,
|
|
80
|
-
env: connection?.type === 'stdio' ? params.env : pluginSettings ?? connection?.env,
|
|
81
|
+
env: connection?.type === 'stdio' ? params.env : (pluginSettings ?? connection?.env),
|
|
81
82
|
params,
|
|
82
83
|
toolName: apiName,
|
|
83
84
|
};
|
|
@@ -89,7 +90,7 @@ class MCPService {
|
|
|
89
90
|
let success = false;
|
|
90
91
|
let errorCode: string | undefined;
|
|
91
92
|
let errorMessage: string | undefined;
|
|
92
|
-
let result:
|
|
93
|
+
let result: MCPToolCallResult | undefined;
|
|
93
94
|
|
|
94
95
|
try {
|
|
95
96
|
// For desktop and stdio, use the desktopClient
|
|
@@ -119,7 +120,7 @@ class MCPService {
|
|
|
119
120
|
|
|
120
121
|
const requestSizeBytes = calculateObjectSizeBytes(inputParams);
|
|
121
122
|
// 计算响应大小
|
|
122
|
-
const responseSizeBytes = success ? calculateObjectSizeBytes(result) : 0;
|
|
123
|
+
const responseSizeBytes = success && result ? calculateObjectSizeBytes(result.state) : 0;
|
|
123
124
|
|
|
124
125
|
const isCustomPlugin = !!customPlugin;
|
|
125
126
|
// 构造上报数据
|
|
@@ -127,10 +128,10 @@ class MCPService {
|
|
|
127
128
|
callDurationMs,
|
|
128
129
|
customPluginInfo: isCustomPlugin
|
|
129
130
|
? {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
avatar: plugin.manifest?.meta.avatar,
|
|
132
|
+
description: plugin.manifest?.meta.description,
|
|
133
|
+
name: plugin.manifest?.meta.title,
|
|
134
|
+
}
|
|
134
135
|
: undefined,
|
|
135
136
|
errorCode,
|
|
136
137
|
errorMessage,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { lambdaClient } from "@/libs/trpc/client";
|
|
2
|
+
|
|
3
|
+
class UsageService {
|
|
4
|
+
findByMonth = async (mo?: string) => {
|
|
5
|
+
return lambdaClient.usage.findByMonth.query({ mo });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
findAndGroupByDay = async (mo?: string) => {
|
|
9
|
+
return lambdaClient.usage.findAndGroupByDay.query({ mo });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const usageService = new UsageService();
|
|
@@ -39,10 +39,9 @@ export const createAgentExecutors = (context: {
|
|
|
39
39
|
traceId?: string;
|
|
40
40
|
};
|
|
41
41
|
parentId: string;
|
|
42
|
-
|
|
42
|
+
skipCreateFirstMessage?: boolean;
|
|
43
43
|
}) => {
|
|
44
|
-
|
|
45
|
-
let shouldSkipCreateMessage = context.parentMessageType === 'assistant';
|
|
44
|
+
let shouldSkipCreateMessage = context.skipCreateFirstMessage;
|
|
46
45
|
|
|
47
46
|
const executors: Partial<Record<AgentInstruction['type'], InstructionExecutor>> = {
|
|
48
47
|
/**
|
|
@@ -44,6 +44,10 @@ export interface ConversationLifecycleAction {
|
|
|
44
44
|
id: string,
|
|
45
45
|
params?: { skipTrace?: boolean; traceId?: string },
|
|
46
46
|
) => Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Continue generating from current assistant message
|
|
49
|
+
*/
|
|
50
|
+
continueGenerationMessage: (lastBlockId: string, messageId: string) => Promise<void>;
|
|
47
51
|
/**
|
|
48
52
|
* Deletes an existing message and generates a new one in its place
|
|
49
53
|
*/
|
|
@@ -75,7 +79,12 @@ export const conversationLifecycle: StateCreator<
|
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
const messages = displayMessageSelectors.activeDisplayMessages(get());
|
|
78
|
-
const
|
|
82
|
+
const lastDisplayMessageId = displayMessageSelectors.lastDisplayMessageId(get());
|
|
83
|
+
|
|
84
|
+
let parentId: string | undefined;
|
|
85
|
+
if (lastDisplayMessageId) {
|
|
86
|
+
parentId = displayMessageSelectors.findLastMessageId(lastDisplayMessageId)(get());
|
|
87
|
+
}
|
|
79
88
|
|
|
80
89
|
const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
|
|
81
90
|
const autoCreateThreshold =
|
|
@@ -230,6 +239,7 @@ export const conversationLifecycle: StateCreator<
|
|
|
230
239
|
parentMessageType: 'assistant',
|
|
231
240
|
ragQuery: get().internal_shouldUseRAG() ? message : undefined,
|
|
232
241
|
threadId: activeThreadId,
|
|
242
|
+
skipCreateFirstMessage: true,
|
|
233
243
|
});
|
|
234
244
|
|
|
235
245
|
//
|
|
@@ -318,6 +328,35 @@ export const conversationLifecycle: StateCreator<
|
|
|
318
328
|
await get().regenerateUserMessage(userId, params);
|
|
319
329
|
},
|
|
320
330
|
|
|
331
|
+
continueGenerationMessage: async (id, messageId) => {
|
|
332
|
+
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
|
333
|
+
if (!message) return;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// Mark message as continuing
|
|
337
|
+
set(
|
|
338
|
+
{ continuingIds: [...get().continuingIds, messageId] },
|
|
339
|
+
false,
|
|
340
|
+
'continueGenerationMessage/start',
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const chats = displayMessageSelectors.mainAIChatsWithHistoryConfig(get());
|
|
344
|
+
|
|
345
|
+
await get().internal_execAgentRuntime({
|
|
346
|
+
messages: chats,
|
|
347
|
+
parentMessageId: id,
|
|
348
|
+
parentMessageType: message.role as 'assistant' | 'tool' | 'user',
|
|
349
|
+
});
|
|
350
|
+
} finally {
|
|
351
|
+
// Remove message from continuing state
|
|
352
|
+
set(
|
|
353
|
+
{ continuingIds: get().continuingIds.filter((msgId) => msgId !== messageId) },
|
|
354
|
+
false,
|
|
355
|
+
'continueGenerationMessage/end',
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
|
|
321
360
|
delAndRegenerateMessage: async (id) => {
|
|
322
361
|
const traceId = dbMessageSelectors.getTraceIdByDbMessageId(id)(get());
|
|
323
362
|
get().regenerateAssistantMessage(id, { skipTrace: true, traceId });
|