@lobehub/chat 1.139.2 → 1.139.4

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 (52) hide show
  1. package/.github/workflows/desktop-pr-build.yml +2 -2
  2. package/.github/workflows/docker-database.yml +1 -1
  3. package/.github/workflows/docker-pglite.yml +1 -1
  4. package/.github/workflows/docker.yml +1 -1
  5. package/.github/workflows/release-desktop-beta.yml +2 -2
  6. package/CHANGELOG.md +50 -0
  7. package/apps/desktop/package.json +1 -1
  8. package/changelog/v1.json +18 -0
  9. package/docs/development/basic/work-with-server-side-database.mdx +5 -5
  10. package/docs/development/basic/work-with-server-side-database.zh-CN.mdx +5 -5
  11. package/docs/development/tests/integration-testing.zh-CN.mdx +399 -0
  12. package/locales/ar/chat.json +3 -1
  13. package/locales/bg-BG/chat.json +3 -1
  14. package/locales/de-DE/chat.json +3 -1
  15. package/locales/en-US/chat.json +3 -1
  16. package/locales/es-ES/chat.json +3 -1
  17. package/locales/fa-IR/chat.json +3 -1
  18. package/locales/fr-FR/chat.json +3 -1
  19. package/locales/it-IT/chat.json +3 -1
  20. package/locales/ja-JP/chat.json +3 -1
  21. package/locales/ko-KR/chat.json +3 -1
  22. package/locales/nl-NL/chat.json +3 -1
  23. package/locales/pl-PL/chat.json +3 -1
  24. package/locales/pt-BR/chat.json +3 -1
  25. package/locales/ru-RU/chat.json +3 -1
  26. package/locales/tr-TR/chat.json +3 -1
  27. package/locales/vi-VN/chat.json +3 -1
  28. package/locales/zh-CN/chat.json +3 -1
  29. package/locales/zh-TW/chat.json +3 -1
  30. package/package.json +2 -2
  31. package/packages/database/package.json +2 -1
  32. package/packages/database/tests/test-utils.ts +1 -0
  33. package/packages/types/src/message/chat.ts +1 -0
  34. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +28 -9
  35. package/src/features/DevPanel/index.tsx +7 -1
  36. package/src/features/ElectronTitlebar/UpdateNotification.tsx +19 -2
  37. package/src/locales/default/chat.ts +2 -0
  38. package/src/server/routers/lambda/{agent.test.ts → __tests__/agent.test.ts} +1 -1
  39. package/src/server/routers/lambda/__tests__/aiChat.test.ts +259 -0
  40. package/src/server/routers/lambda/{aiModel.test.ts → __tests__/aiModel.test.ts} +1 -1
  41. package/src/server/routers/lambda/{aiProvider.test.ts → __tests__/aiProvider.test.ts} +1 -1
  42. package/src/server/routers/lambda/{generation.test.ts → __tests__/generation.test.ts} +1 -1
  43. package/src/server/routers/lambda/{generationBatch.test.ts → __tests__/generationBatch.test.ts} +1 -1
  44. package/src/server/routers/lambda/{generationTopic.test.ts → __tests__/generationTopic.test.ts} +1 -1
  45. package/src/server/routers/lambda/__tests__/integration/README.md +110 -0
  46. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +545 -0
  47. package/src/server/routers/lambda/__tests__/integration/setup.ts +36 -0
  48. package/src/server/routers/lambda/{user.test.ts → __tests__/user.test.ts} +1 -1
  49. package/src/server/routers/lambda/aiChat.ts +2 -0
  50. package/src/store/chat/slices/message/action.test.ts +92 -0
  51. package/src/store/chat/slices/message/action.ts +3 -1
  52. package/src/server/routers/lambda/aiChat.test.ts +0 -108
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "رفتن به پیام شماره {{index}}",
230
230
  "nextMessage": "پیام بعدی",
231
- "previousMessage": "پیام قبلی"
231
+ "previousMessage": "پیام قبلی",
232
+ "senderAssistant": "دستیار",
233
+ "senderUser": "شما"
232
234
  },
233
235
  "newAgent": "دستیار جدید",
234
236
  "newGroupChat": "ایجاد چت گروهی جدید",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "Aller au message n° {{index}}",
230
230
  "nextMessage": "Message suivant",
231
- "previousMessage": "Message précédent"
231
+ "previousMessage": "Message précédent",
232
+ "senderAssistant": "Assistant",
233
+ "senderUser": "Vous"
232
234
  },
233
235
  "newAgent": "Nouvel agent",
234
236
  "newGroupChat": "Nouveau groupe de discussion",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "Vai al messaggio n. {{index}}",
230
230
  "nextMessage": "Messaggio successivo",
231
- "previousMessage": "Messaggio precedente"
231
+ "previousMessage": "Messaggio precedente",
232
+ "senderAssistant": "Assistente",
233
+ "senderUser": "Tu"
232
234
  },
233
235
  "newAgent": "Nuovo assistente",
234
236
  "newGroupChat": "Nuova chat di gruppo",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "メッセージ {{index}} へジャンプ",
230
230
  "nextMessage": "次のメッセージ",
231
- "previousMessage": "前のメッセージ"
231
+ "previousMessage": "前のメッセージ",
232
+ "senderAssistant": "アシスタント",
233
+ "senderUser": "あなた"
232
234
  },
233
235
  "newAgent": "新しいエージェント",
234
236
  "newGroupChat": "新しいグループチャットを作成",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "{{index}}번째 메시지로 이동",
230
230
  "nextMessage": "다음 메시지",
231
- "previousMessage": "이전 메시지"
231
+ "previousMessage": "이전 메시지",
232
+ "senderAssistant": "도우미",
233
+ "senderUser": "당신"
232
234
  },
233
235
  "newAgent": "새 도우미",
234
236
  "newGroupChat": "새 그룹 채팅 만들기",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "Ga naar bericht {{index}}",
230
230
  "nextMessage": "Volgend bericht",
231
- "previousMessage": "Vorig bericht"
231
+ "previousMessage": "Vorig bericht",
232
+ "senderAssistant": "Assistent",
233
+ "senderUser": "Jij"
232
234
  },
233
235
  "newAgent": "Nieuwe assistent",
234
236
  "newGroupChat": "Nieuwe groepschat",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "Przejdź do wiadomości nr {{index}}",
230
230
  "nextMessage": "Następna wiadomość",
231
- "previousMessage": "Poprzednia wiadomość"
231
+ "previousMessage": "Poprzednia wiadomość",
232
+ "senderAssistant": "Asystent",
233
+ "senderUser": "Ty"
232
234
  },
233
235
  "newAgent": "Nowy asystent",
234
236
  "newGroupChat": "Utwórz nowy czat grupowy",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "Ir para a mensagem nº {{index}}",
230
230
  "nextMessage": "Próxima mensagem",
231
- "previousMessage": "Mensagem anterior"
231
+ "previousMessage": "Mensagem anterior",
232
+ "senderAssistant": "Assistente",
233
+ "senderUser": "Você"
232
234
  },
233
235
  "newAgent": "Novo Assistente",
234
236
  "newGroupChat": "Criar novo grupo",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "Перейти к сообщению № {{index}}",
230
230
  "nextMessage": "Следующее сообщение",
231
- "previousMessage": "Предыдущее сообщение"
231
+ "previousMessage": "Предыдущее сообщение",
232
+ "senderAssistant": "Ассистент",
233
+ "senderUser": "Вы"
232
234
  },
233
235
  "newAgent": "Создать помощника",
234
236
  "newGroupChat": "Создать групповой чат",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "{{index}} numaralı mesaja atla",
230
230
  "nextMessage": "Sonraki mesaj",
231
- "previousMessage": "Önceki mesaj"
231
+ "previousMessage": "Önceki mesaj",
232
+ "senderAssistant": "Asistan",
233
+ "senderUser": "Sen"
232
234
  },
233
235
  "newAgent": "Yeni Asistan",
234
236
  "newGroupChat": "Yeni grup sohbeti oluştur",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "Chuyển đến tin nhắn thứ {{index}}",
230
230
  "nextMessage": "Tin nhắn tiếp theo",
231
- "previousMessage": "Tin nhắn trước"
231
+ "previousMessage": "Tin nhắn trước",
232
+ "senderAssistant": "Trợ lý",
233
+ "senderUser": "Bạn"
232
234
  },
233
235
  "newAgent": "Tạo trợ lý mới",
234
236
  "newGroupChat": "Tạo nhóm mới",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "跳转至第 {{index}} 条消息",
230
230
  "nextMessage": "下一条消息",
231
- "previousMessage": "上一条消息"
231
+ "previousMessage": "上一条消息",
232
+ "senderAssistant": "助手",
233
+ "senderUser": "你"
232
234
  },
233
235
  "newAgent": "新建助手",
234
236
  "newGroupChat": "新建群聊",
@@ -228,7 +228,9 @@
228
228
  "minimap": {
229
229
  "jumpToMessage": "跳轉至第 {{index}} 條訊息",
230
230
  "nextMessage": "下一條訊息",
231
- "previousMessage": "上一條訊息"
231
+ "previousMessage": "上一條訊息",
232
+ "senderAssistant": "助理",
233
+ "senderUser": "您"
232
234
  },
233
235
  "newAgent": "新建助手",
234
236
  "newGroupChat": "建立群組",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.139.2",
3
+ "version": "1.139.4",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -238,7 +238,7 @@
238
238
  "oidc-provider": "^9.5.1",
239
239
  "ollama": "^0.6.0",
240
240
  "openai": "^4.104.0",
241
- "openapi-fetch": "^0.9.8",
241
+ "openapi-fetch": "^0.14.0",
242
242
  "partial-json": "^0.1.7",
243
243
  "path-browserify-esm": "^1.0.6",
244
244
  "pdf-parse": "^1.1.1",
@@ -4,7 +4,8 @@
4
4
  "private": true,
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
7
- "./schemas": "./src/schemas/index.ts"
7
+ "./schemas": "./src/schemas/index.ts",
8
+ "./test-utils": "./tests/test-utils.ts"
8
9
  },
9
10
  "scripts": {
10
11
  "test": "npm run test:client-db && npm run test:server-db",
@@ -0,0 +1 @@
1
+ export * from '../src/models/__tests__/_util';
@@ -138,6 +138,7 @@ export interface CreateMessageParams
138
138
  role: MessageRoleType;
139
139
  sessionId: string;
140
140
  targetId?: string | null;
141
+ threadId?: string | null;
141
142
  topicId?: string;
142
143
  traceId?: string;
143
144
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Icon } from '@lobehub/ui';
4
- import { Tooltip } from 'antd';
4
+ import { Popover, Tooltip } from 'antd';
5
5
  import { createStyles, useTheme } from 'antd-style';
6
6
  import debug from 'debug';
7
7
  import { ChevronDown, ChevronUp } from 'lucide-react';
@@ -114,6 +114,19 @@ const useStyles = createStyles(({ css, token }) => ({
114
114
  indicatorContentActive: css`
115
115
  background: ${token.colorPrimary};
116
116
  `,
117
+ popoverContent: css`
118
+ max-width: 300px;
119
+ `,
120
+ popoverLabel: css`
121
+ margin-block-end: 4px;
122
+ font-size: 12px;
123
+ font-weight: 600;
124
+ color: ${token.colorTextSecondary};
125
+ `,
126
+ popoverText: css`
127
+ color: ${token.colorText};
128
+ word-break: break-word;
129
+ `,
117
130
  rail: css`
118
131
  pointer-events: auto;
119
132
 
@@ -174,6 +187,7 @@ const getPreviewText = (content: string | undefined) => {
174
187
  interface MinimapIndicator {
175
188
  id: string;
176
189
  preview: string;
190
+ role: 'user' | 'assistant';
177
191
  virtuosoIndex: number;
178
192
  width: number;
179
193
  }
@@ -203,6 +217,7 @@ const ChatMinimap = () => {
203
217
  acc.push({
204
218
  id: message.id,
205
219
  preview: getPreviewText(message.content),
220
+ role: message.role,
206
221
  virtuosoIndex,
207
222
  width: getIndicatorWidth(message.content),
208
223
  });
@@ -317,16 +332,20 @@ const ChatMinimap = () => {
317
332
  </button>
318
333
  </Tooltip>
319
334
  <Flexbox className={styles.railContent}>
320
- {indicators.map(({ id, width, preview, virtuosoIndex }, position) => {
335
+ {indicators.map(({ id, width, preview, role, virtuosoIndex }, position) => {
321
336
  const isActive = activeIndicatorPosition === position;
337
+ const senderLabel =
338
+ role === 'user' ? t('minimap.senderUser') : t('minimap.senderAssistant');
339
+
340
+ const popoverContent = preview ? (
341
+ <div className={styles.popoverContent}>
342
+ <div className={styles.popoverLabel}>{senderLabel}</div>
343
+ <div className={styles.popoverText}>{preview}</div>
344
+ </div>
345
+ ) : undefined;
322
346
 
323
347
  return (
324
- <Tooltip
325
- key={id}
326
- mouseEnterDelay={0.1}
327
- placement={'left'}
328
- title={preview || undefined}
329
- >
348
+ <Popover content={popoverContent} key={id} mouseEnterDelay={0.1} placement={'left'}>
330
349
  <button
331
350
  aria-current={isActive ? 'true' : undefined}
332
351
  aria-label={t('minimap.jumpToMessage', { index: position + 1 })}
@@ -344,7 +363,7 @@ const ChatMinimap = () => {
344
363
  )}
345
364
  />
346
365
  </button>
347
- </Tooltip>
366
+ </Popover>
348
367
  );
349
368
  })}
350
369
  </Flexbox>
@@ -1,11 +1,17 @@
1
+ 'use client';
2
+
1
3
  import { BookText, Cog, DatabaseIcon, FlagIcon, GlobeLockIcon } from 'lucide-react';
4
+ import dynamic from 'next/dynamic';
2
5
 
3
6
  import CacheViewer from './CacheViewer';
4
7
  import FeatureFlagViewer from './FeatureFlagViewer';
5
8
  import MetadataViewer from './MetadataViewer';
6
9
  import PostgresViewer from './PostgresViewer';
7
10
  import SystemInspector from './SystemInspector';
8
- import FloatPanel from './features/FloatPanel';
11
+
12
+ const FloatPanel = dynamic(() => import('./features/FloatPanel'), {
13
+ ssr: false,
14
+ });
9
15
 
10
16
  const DevPanel = () => (
11
17
  <FloatPanel
@@ -39,6 +39,7 @@ export const UpdateNotification: React.FC = () => {
39
39
  'unconfirm' | 'installLater' | 'installNow' | null
40
40
  >('unconfirm');
41
41
  const [detailVisible, setDetailVisible] = useState(false);
42
+ const [isInstalling, setIsInstalling] = useState(false);
42
43
 
43
44
  useWatchBroadcast('updateDownloaded', (info: UpdateInfo) => {
44
45
  setUpdateInfo(info);
@@ -110,7 +111,15 @@ export const UpdateNotification: React.FC = () => {
110
111
  {t('updater.later')}
111
112
  </Button>
112
113
 
113
- <Button onClick={() => autoUpdateService.installNow()} size="small" type="primary">
114
+ <Button
115
+ loading={isInstalling}
116
+ onClick={() => {
117
+ setIsInstalling(true);
118
+ autoUpdateService.installNow();
119
+ }}
120
+ size="small"
121
+ type="primary"
122
+ >
114
123
  {t('updater.upgradeNow')}
115
124
  </Button>
116
125
  </div>
@@ -137,7 +146,15 @@ export const UpdateNotification: React.FC = () => {
137
146
  <Button onClick={() => autoUpdateService.installLater()} size="small">
138
147
  {t('updater.installLater')}
139
148
  </Button>
140
- <Button onClick={() => autoUpdateService.installNow()} size="small" type="primary">
149
+ <Button
150
+ loading={isInstalling}
151
+ onClick={() => {
152
+ setIsInstalling(true);
153
+ autoUpdateService.installNow();
154
+ }}
155
+ size="small"
156
+ type="primary"
157
+ >
141
158
  {t('updater.restartAndInstall', '立即安装')}
142
159
  </Button>
143
160
  </div>
@@ -247,6 +247,8 @@ export default {
247
247
  jumpToMessage: '跳转至第 {{index}} 条消息',
248
248
  nextMessage: '下一条消息',
249
249
  previousMessage: '上一条消息',
250
+ senderAssistant: '助手',
251
+ senderUser: '你',
250
252
  },
251
253
 
252
254
  newAgent: '新建助手',
@@ -12,7 +12,7 @@ import { serverDB } from '@/database/server';
12
12
  import { AgentService } from '@/server/services/agent';
13
13
  import { KnowledgeType } from '@/types/knowledgeBase';
14
14
 
15
- import { agentRouter } from './agent';
15
+ import { agentRouter } from '../agent';
16
16
 
17
17
  vi.mock('@/database/models/user', () => ({
18
18
  UserModel: {
@@ -0,0 +1,259 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { MessageModel } from '@/database/models/message';
5
+ import { TopicModel } from '@/database/models/topic';
6
+ import { AiChatService } from '@/server/services/aiChat';
7
+
8
+ import { aiChatRouter } from '../aiChat';
9
+
10
+ vi.mock('@/database/models/message');
11
+ vi.mock('@/database/models/topic');
12
+ vi.mock('@/server/services/aiChat');
13
+ vi.mock('@/server/services/file', () => ({
14
+ FileService: vi.fn(),
15
+ }));
16
+ vi.mock('@/utils/server', () => ({
17
+ getXorPayload: vi.fn(),
18
+ }));
19
+ vi.mock('@/server/modules/ModelRuntime', () => ({
20
+ initModelRuntimeWithUserPayload: vi.fn(),
21
+ }));
22
+
23
+ describe('aiChatRouter', () => {
24
+ const mockCtx = { userId: 'u1' };
25
+
26
+ it('should create topic optionally, create user/assistant messages, and return payload', async () => {
27
+ const mockCreateTopic = vi.fn().mockResolvedValue({ id: 't1' });
28
+ const mockCreateMessage = vi
29
+ .fn()
30
+ .mockResolvedValueOnce({ id: 'm-user' })
31
+ .mockResolvedValueOnce({ id: 'm-assistant' });
32
+ const mockGet = vi
33
+ .fn()
34
+ .mockResolvedValue({ messages: [{ id: 'm-user' }, { id: 'm-assistant' }], topics: [{}] });
35
+
36
+ vi.mocked(TopicModel).mockImplementation(() => ({ create: mockCreateTopic }) as any);
37
+ vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
38
+ vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
39
+
40
+ const caller = aiChatRouter.createCaller(mockCtx as any);
41
+
42
+ const input = {
43
+ newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
44
+ newTopic: { title: 'T', topicMessageIds: ['a', 'b'] },
45
+ newUserMessage: { content: 'hi', files: ['f1'] },
46
+ sessionId: 's1',
47
+ } as any;
48
+
49
+ const res = await caller.sendMessageInServer(input);
50
+
51
+ expect(mockCreateTopic).toHaveBeenCalledWith({
52
+ messages: ['a', 'b'],
53
+ sessionId: 's1',
54
+ title: 'T',
55
+ });
56
+
57
+ expect(mockCreateMessage).toHaveBeenNthCalledWith(1, {
58
+ content: 'hi',
59
+ files: ['f1'],
60
+ role: 'user',
61
+ sessionId: 's1',
62
+ topicId: 't1',
63
+ });
64
+
65
+ expect(mockCreateMessage).toHaveBeenNthCalledWith(
66
+ 2,
67
+ expect.objectContaining({
68
+ content: expect.any(String),
69
+ fromModel: 'gpt-4o',
70
+ parentId: 'm-user',
71
+ role: 'assistant',
72
+ sessionId: 's1',
73
+ topicId: 't1',
74
+ }),
75
+ );
76
+
77
+ expect(mockGet).toHaveBeenCalledWith({ includeTopic: true, sessionId: 's1', topicId: 't1' });
78
+ expect(res.assistantMessageId).toBe('m-assistant');
79
+ expect(res.userMessageId).toBe('m-user');
80
+ expect(res.isCreateNewTopic).toBe(true);
81
+ expect(res.topicId).toBe('t1');
82
+ expect(res.messages?.length).toBe(2);
83
+ expect(res.topics?.length).toBe(1);
84
+ });
85
+
86
+ it('should reuse existing topic when topicId provided', async () => {
87
+ const mockCreateMessage = vi
88
+ .fn()
89
+ .mockResolvedValueOnce({ id: 'm-user' })
90
+ .mockResolvedValueOnce({ id: 'm-assistant' });
91
+ const mockGet = vi.fn().mockResolvedValue({ messages: [], topics: undefined });
92
+
93
+ vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
94
+ vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
95
+
96
+ const caller = aiChatRouter.createCaller(mockCtx as any);
97
+
98
+ const res = await caller.sendMessageInServer({
99
+ newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
100
+ newUserMessage: { content: 'hi' },
101
+ sessionId: 's1',
102
+ topicId: 't-exist',
103
+ } as any);
104
+
105
+ expect(mockCreateMessage).toHaveBeenCalled();
106
+ expect(mockGet).toHaveBeenCalledWith({
107
+ includeTopic: false,
108
+ sessionId: 's1',
109
+ topicId: 't-exist',
110
+ });
111
+ expect(res.isCreateNewTopic).toBe(false);
112
+ expect(res.topicId).toBe('t-exist');
113
+ });
114
+
115
+ it('should pass threadId to both user and assistant messages when provided', async () => {
116
+ const mockCreateMessage = vi
117
+ .fn()
118
+ .mockResolvedValueOnce({ id: 'm-user' })
119
+ .mockResolvedValueOnce({ id: 'm-assistant' });
120
+ const mockGet = vi.fn().mockResolvedValue({ messages: [], topics: undefined });
121
+
122
+ vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
123
+ vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
124
+
125
+ const caller = aiChatRouter.createCaller(mockCtx as any);
126
+
127
+ await caller.sendMessageInServer({
128
+ newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
129
+ newUserMessage: { content: 'hi' },
130
+ sessionId: 's1',
131
+ threadId: 'thread-123',
132
+ topicId: 't1',
133
+ } as any);
134
+
135
+ expect(mockCreateMessage).toHaveBeenNthCalledWith(1, {
136
+ content: 'hi',
137
+ role: 'user',
138
+ sessionId: 's1',
139
+ threadId: 'thread-123',
140
+ topicId: 't1',
141
+ });
142
+
143
+ expect(mockCreateMessage).toHaveBeenNthCalledWith(
144
+ 2,
145
+ expect.objectContaining({
146
+ parentId: 'm-user',
147
+ role: 'assistant',
148
+ sessionId: 's1',
149
+ threadId: 'thread-123',
150
+ topicId: 't1',
151
+ }),
152
+ );
153
+ });
154
+
155
+ describe('outputJSON', () => {
156
+ it('should successfully generate structured output', async () => {
157
+ const { getXorPayload } = await import('@/utils/server');
158
+ const { initModelRuntimeWithUserPayload } = await import('@/server/modules/ModelRuntime');
159
+
160
+ const mockPayload = { apiKey: 'test-key' };
161
+ const mockResult = { object: { name: 'John', age: 30 } };
162
+ const mockGenerateObject = vi.fn().mockResolvedValue(mockResult);
163
+
164
+ vi.mocked(getXorPayload).mockReturnValue(mockPayload);
165
+ vi.mocked(initModelRuntimeWithUserPayload).mockReturnValue({
166
+ generateObject: mockGenerateObject,
167
+ } as any);
168
+
169
+ const caller = aiChatRouter.createCaller(mockCtx as any);
170
+
171
+ const input = {
172
+ keyVaultsPayload: 'encrypted-payload',
173
+ messages: [{ content: 'test', role: 'user' }],
174
+ model: 'gpt-4o',
175
+ provider: 'openai',
176
+ schema: {
177
+ name: 'Person',
178
+ schema: {
179
+ type: 'object' as const,
180
+ properties: { name: { type: 'string' }, age: { type: 'number' } },
181
+ },
182
+ },
183
+ };
184
+
185
+ const result = await caller.outputJSON(input);
186
+
187
+ expect(getXorPayload).toHaveBeenCalledWith('encrypted-payload');
188
+ expect(initModelRuntimeWithUserPayload).toHaveBeenCalledWith('openai', mockPayload);
189
+ expect(mockGenerateObject).toHaveBeenCalledWith({
190
+ messages: input.messages,
191
+ model: 'gpt-4o',
192
+ schema: input.schema,
193
+ tools: undefined,
194
+ });
195
+ expect(result).toEqual(mockResult);
196
+ });
197
+
198
+ it('should throw error when keyVaultsPayload is invalid', async () => {
199
+ const { getXorPayload } = await import('@/utils/server');
200
+
201
+ vi.mocked(getXorPayload).mockReturnValue(undefined as any);
202
+
203
+ const caller = aiChatRouter.createCaller(mockCtx as any);
204
+
205
+ const input = {
206
+ keyVaultsPayload: 'invalid-payload',
207
+ messages: [],
208
+ model: 'gpt-4o',
209
+ provider: 'openai',
210
+ };
211
+
212
+ await expect(caller.outputJSON(input)).rejects.toThrow('keyVaultsPayload is not correct');
213
+ });
214
+
215
+ it('should handle tools parameter when provided', async () => {
216
+ const { getXorPayload } = await import('@/utils/server');
217
+ const { initModelRuntimeWithUserPayload } = await import('@/server/modules/ModelRuntime');
218
+
219
+ const mockPayload = { apiKey: 'test-key' };
220
+ const mockTools = [
221
+ {
222
+ type: 'function' as const,
223
+ function: {
224
+ name: 'test',
225
+ parameters: {
226
+ type: 'object' as const,
227
+ properties: { input: { type: 'string' } },
228
+ },
229
+ },
230
+ },
231
+ ];
232
+ const mockGenerateObject = vi.fn().mockResolvedValue({ object: {} });
233
+
234
+ vi.mocked(getXorPayload).mockReturnValue(mockPayload);
235
+ vi.mocked(initModelRuntimeWithUserPayload).mockReturnValue({
236
+ generateObject: mockGenerateObject,
237
+ } as any);
238
+
239
+ const caller = aiChatRouter.createCaller(mockCtx as any);
240
+
241
+ const input = {
242
+ keyVaultsPayload: 'encrypted-payload',
243
+ messages: [],
244
+ model: 'gpt-4o',
245
+ provider: 'openai',
246
+ tools: mockTools,
247
+ };
248
+
249
+ await caller.outputJSON(input);
250
+
251
+ expect(mockGenerateObject).toHaveBeenCalledWith({
252
+ messages: [],
253
+ model: 'gpt-4o',
254
+ schema: undefined,
255
+ tools: mockTools,
256
+ });
257
+ });
258
+ });
259
+ });
@@ -4,7 +4,7 @@ import { AiModelModel } from '@/database/models/aiModel';
4
4
  import { UserModel } from '@/database/models/user';
5
5
  import { AiInfraRepos } from '@/database/repositories/aiInfra';
6
6
 
7
- import { aiModelRouter } from './aiModel';
7
+ import { aiModelRouter } from '../aiModel';
8
8
 
9
9
  vi.mock('@/database/models/aiModel');
10
10
  vi.mock('@/database/models/user');
@@ -6,7 +6,7 @@ import { getServerGlobalConfig } from '@/server/globalConfig';
6
6
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
7
7
  import { AiProviderDetailItem, AiProviderRuntimeState } from '@/types/aiProvider';
8
8
 
9
- import { aiProviderRouter } from './aiProvider';
9
+ import { aiProviderRouter } from '../aiProvider';
10
10
 
11
11
  vi.mock('@/server/globalConfig');
12
12
  vi.mock('@/server/modules/KeyVaultsEncrypt');
@@ -6,7 +6,7 @@ import { GenerationModel } from '@/database/models/generation';
6
6
  import { FileService } from '@/server/services/file';
7
7
  import { AsyncTaskStatus } from '@/types/asyncTask';
8
8
 
9
- import { generationRouter } from './generation';
9
+ import { generationRouter } from '../generation';
10
10
 
11
11
  vi.mock('@/database/models/asyncTask');
12
12
  vi.mock('@/database/models/generation');
@@ -4,7 +4,7 @@ import { GenerationBatchModel } from '@/database/models/generationBatch';
4
4
  import { GenerationBatchItem } from '@/database/schemas/generation';
5
5
  import { FileService } from '@/server/services/file';
6
6
 
7
- import { generationBatchRouter } from './generationBatch';
7
+ import { generationBatchRouter } from '../generationBatch';
8
8
 
9
9
  vi.mock('@/database/models/generationBatch');
10
10
  vi.mock('@/server/services/file');