@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/modules/networkProxy/__tests__/dispatcher.test.ts +401 -0
  3. package/apps/desktop/src/main/modules/networkProxy/__tests__/tester.test.ts +531 -0
  4. package/apps/desktop/src/main/modules/networkProxy/__tests__/urlBuilder.test.ts +349 -0
  5. package/apps/desktop/src/main/modules/networkProxy/__tests__/validator.test.ts +492 -0
  6. package/changelog/v1.json +14 -0
  7. package/locales/ar/auth.json +45 -1
  8. package/locales/ar/modelProvider.json +13 -1
  9. package/locales/bg-BG/auth.json +45 -1
  10. package/locales/bg-BG/modelProvider.json +13 -1
  11. package/locales/de-DE/auth.json +45 -1
  12. package/locales/de-DE/modelProvider.json +13 -1
  13. package/locales/en-US/auth.json +45 -1
  14. package/locales/en-US/modelProvider.json +13 -1
  15. package/locales/es-ES/auth.json +45 -1
  16. package/locales/es-ES/modelProvider.json +13 -1
  17. package/locales/fa-IR/auth.json +45 -1
  18. package/locales/fa-IR/modelProvider.json +13 -1
  19. package/locales/fr-FR/auth.json +45 -1
  20. package/locales/fr-FR/modelProvider.json +13 -1
  21. package/locales/it-IT/auth.json +45 -1
  22. package/locales/it-IT/modelProvider.json +13 -1
  23. package/locales/ja-JP/auth.json +45 -1
  24. package/locales/ja-JP/modelProvider.json +13 -1
  25. package/locales/ko-KR/auth.json +45 -1
  26. package/locales/ko-KR/modelProvider.json +13 -1
  27. package/locales/nl-NL/auth.json +45 -1
  28. package/locales/nl-NL/modelProvider.json +13 -1
  29. package/locales/pl-PL/auth.json +45 -1
  30. package/locales/pl-PL/modelProvider.json +13 -1
  31. package/locales/pt-BR/auth.json +45 -1
  32. package/locales/pt-BR/modelProvider.json +13 -1
  33. package/locales/ru-RU/auth.json +45 -1
  34. package/locales/ru-RU/modelProvider.json +13 -1
  35. package/locales/tr-TR/auth.json +45 -1
  36. package/locales/tr-TR/modelProvider.json +13 -1
  37. package/locales/vi-VN/auth.json +45 -1
  38. package/locales/vi-VN/modelProvider.json +13 -1
  39. package/locales/zh-CN/auth.json +45 -1
  40. package/locales/zh-CN/modelProvider.json +13 -1
  41. package/locales/zh-TW/auth.json +45 -1
  42. package/locales/zh-TW/modelProvider.json +13 -1
  43. package/package.json +1 -1
  44. package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
  45. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
  46. package/packages/obervability-otel/package.json +3 -1
  47. package/packages/obervability-otel/src/api.ts +2 -0
  48. package/packages/obervability-otel/src/trpc/convention.ts +16 -0
  49. package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
  50. package/packages/obervability-otel/src/trpc/index.ts +62 -0
  51. package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
  52. package/packages/types/src/usage/usageRecord.ts +54 -0
  53. package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
  54. package/packages/web-crawler/src/crawImpl/naive.ts +9 -9
  55. package/packages/web-crawler/src/crawler.ts +5 -5
  56. package/packages/web-crawler/src/urlRules.ts +13 -13
  57. package/packages/web-crawler/src/utils/appUrlRules.ts +5 -5
  58. package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
  59. package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
  60. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
  61. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
  62. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
  63. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
  64. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
  65. package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
  66. package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
  67. package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
  68. package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
  69. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
  70. package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
  71. package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
  72. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
  73. package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
  74. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
  75. package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
  76. package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
  77. package/src/features/Conversation/Messages/Group/index.tsx +7 -2
  78. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +3 -0
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
  80. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  81. package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
  82. package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
  83. package/src/features/PluginsUI/Render/index.tsx +17 -0
  84. package/src/libs/mcp/client.ts +3 -2
  85. package/src/libs/mcp/types.ts +71 -0
  86. package/src/libs/trpc/lambda/index.ts +5 -2
  87. package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
  88. package/src/locales/default/auth.ts +44 -0
  89. package/src/locales/default/chat.ts +1 -0
  90. package/src/server/routers/desktop/mcp.ts +1 -3
  91. package/src/server/routers/lambda/index.ts +2 -0
  92. package/src/server/routers/lambda/usage.ts +36 -0
  93. package/src/server/routers/tools/mcp.ts +1 -3
  94. package/src/server/services/mcp/index.test.ts +28 -15
  95. package/src/server/services/mcp/index.ts +29 -18
  96. package/src/server/services/usage/index.test.ts +310 -0
  97. package/src/server/services/usage/index.ts +164 -0
  98. package/src/services/chat/contextEngineering.test.ts +4 -0
  99. package/src/services/mcp.test.ts +7 -1
  100. package/src/services/mcp.ts +13 -12
  101. package/src/services/usage.ts +13 -0
  102. package/src/store/chat/agents/createAgentExecutors.ts +2 -3
  103. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
  104. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
  105. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
  106. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
  107. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
  108. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  109. package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
  110. package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
  111. package/src/store/chat/slices/message/initialState.ts +5 -0
  112. package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
  113. package/src/store/chat/slices/message/selectors/chat.ts +0 -2
  114. package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
  115. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
  116. package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
  117. package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
  118. package/src/store/chat/slices/plugin/action.test.ts +4 -4
  119. package/src/store/chat/slices/plugin/actions/index.ts +39 -0
  120. package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
  121. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
  122. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
  123. package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
  124. package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
  125. package/src/store/chat/store.ts +1 -1
  126. package/src/store/global/initialState.ts +1 -0
  127. 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
  ]);
@@ -305,7 +305,13 @@ describe('MCPService', () => {
305
305
  mockPluginSelectors.getInstalledPluginById.mockReturnValue(() => mockPlugin);
306
306
  mockPluginSelectors.getCustomPluginById.mockReturnValue(() => null);
307
307
 
308
- const mockResult = 'response data';
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 = {
@@ -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
- ([, value]) => value !== undefined && value !== null,
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
- acc[key] = value;
54
+ acc[key] = value;
54
55
 
55
- return acc;
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: any;
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
- avatar: plugin.manifest?.meta.avatar,
131
- description: plugin.manifest?.meta.description,
132
- name: plugin.manifest?.meta.title,
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
- parentMessageType: 'user' | 'assistant';
42
+ skipCreateFirstMessage?: boolean;
43
43
  }) => {
44
- // 当通过 sendMessageInServer 的时候,已经有一条消息了,那么就不需要触发创建
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 parentId = displayMessageSelectors.lastDisplayMessageId(get());
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 });