@lobehub/lobehub 2.0.0-next.276 → 2.0.0-next.278

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 (98) hide show
  1. package/.cursor/rules/db-migrations.mdc +1 -1
  2. package/.cursor/rules/debug-usage.mdc +7 -5
  3. package/.cursor/rules/desktop-controller-tests.mdc +2 -1
  4. package/.cursor/rules/desktop-feature-implementation.mdc +9 -5
  5. package/.cursor/rules/desktop-local-tools-implement.mdc +67 -66
  6. package/.cursor/rules/desktop-menu-configuration.mdc +21 -9
  7. package/.cursor/rules/desktop-window-management.mdc +17 -2
  8. package/.cursor/rules/drizzle-schema-style-guide.mdc +6 -6
  9. package/.cursor/rules/hotkey.mdc +1 -0
  10. package/.cursor/rules/i18n.mdc +1 -0
  11. package/.cursor/rules/project-structure.mdc +16 -3
  12. package/.cursor/rules/react.mdc +17 -5
  13. package/.cursor/rules/recent-data-usage.mdc +2 -1
  14. package/.cursor/rules/testing-guide/testing-guide.mdc +262 -238
  15. package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +1 -1
  16. package/.cursor/rules/zustand-action-patterns.mdc +1 -1
  17. package/.cursor/rules/zustand-slice-organization.mdc +4 -4
  18. package/CHANGELOG.md +51 -0
  19. package/CLAUDE.md +1 -1
  20. package/GEMINI.md +1 -1
  21. package/changelog/v1.json +14 -0
  22. package/docs/development/database-schema.dbml +16 -0
  23. package/locales/en-US/chat.json +24 -0
  24. package/locales/en-US/setting.json +11 -0
  25. package/locales/zh-CN/chat.json +24 -0
  26. package/locales/zh-CN/setting.json +11 -0
  27. package/package.json +1 -1
  28. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/BatchCreateAgents/index.tsx +2 -2
  29. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/UpdateGroup/index.tsx +56 -56
  30. package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +3 -2
  31. package/packages/builtin-tool-group-agent-builder/src/executor.ts +2 -1
  32. package/packages/business/const/src/index.ts +3 -0
  33. package/packages/database/migrations/0069_add_topic_shares_table.sql +22 -0
  34. package/packages/database/migrations/meta/0069_snapshot.json +9704 -0
  35. package/packages/database/migrations/meta/_journal.json +7 -0
  36. package/packages/database/src/models/__tests__/topicShare.test.ts +318 -0
  37. package/packages/database/src/models/topicShare.ts +177 -0
  38. package/packages/database/src/schemas/topic.ts +44 -2
  39. package/packages/types/src/agentCronJob/index.ts +19 -23
  40. package/packages/types/src/conversation.ts +5 -0
  41. package/packages/types/src/serverConfig.ts +1 -0
  42. package/packages/types/src/topic/topic.ts +46 -0
  43. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/Actions.tsx +31 -0
  44. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +10 -6
  45. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/index.tsx +7 -11
  46. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/useDropdownMenu.tsx +102 -0
  47. package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +179 -0
  48. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +111 -0
  49. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobHeader.tsx +45 -0
  50. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +31 -0
  51. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +213 -0
  52. package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +186 -344
  53. package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +24 -9
  54. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/index.tsx +42 -97
  55. package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +4 -20
  56. package/src/app/[variants]/(main)/community/features/UserAvatar/index.tsx +15 -5
  57. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/AgentProfilePopup.tsx +1 -6
  58. package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +26 -9
  59. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/AspectRatioSelect/index.tsx +1 -2
  60. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ImageNum.tsx +54 -173
  61. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ResolutionSelect.tsx +22 -67
  62. package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +18 -0
  63. package/src/app/[variants]/router/desktopRouter.config.tsx +18 -0
  64. package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +54 -0
  65. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +170 -0
  66. package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +66 -0
  67. package/src/app/[variants]/share/t/[id]/index.tsx +112 -0
  68. package/src/app/robots.tsx +1 -1
  69. package/src/business/client/BusinessMobileRoutes.tsx +1 -1
  70. package/src/features/Conversation/ChatList/index.tsx +12 -5
  71. package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +8 -4
  72. package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +15 -10
  73. package/src/features/Conversation/Messages/AssistantGroup/Tools.tsx +3 -1
  74. package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +3 -2
  75. package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +2 -2
  76. package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +25 -26
  77. package/src/features/Conversation/Messages/Supervisor/components/Group.tsx +4 -2
  78. package/src/features/Conversation/Messages/Tool/Tool/index.tsx +16 -12
  79. package/src/features/Conversation/Messages/Tool/index.tsx +20 -11
  80. package/src/features/Conversation/Messages/index.tsx +1 -1
  81. package/src/features/Conversation/store/slices/data/action.ts +2 -1
  82. package/src/features/SharePopover/index.tsx +215 -0
  83. package/src/features/SharePopover/style.ts +10 -0
  84. package/src/libs/next/proxy/define-config.ts +4 -1
  85. package/src/locales/default/chat.ts +26 -0
  86. package/src/proxy.ts +1 -0
  87. package/src/server/globalConfig/index.ts +1 -0
  88. package/src/server/routers/lambda/__tests__/message.test.ts +152 -0
  89. package/src/server/routers/lambda/__tests__/share.test.ts +227 -0
  90. package/src/server/routers/lambda/__tests__/topic.test.ts +174 -0
  91. package/src/server/routers/lambda/index.ts +2 -0
  92. package/src/server/routers/lambda/message.ts +37 -4
  93. package/src/server/routers/lambda/share.ts +55 -0
  94. package/src/server/routers/lambda/topic.ts +45 -0
  95. package/src/services/chatGroup/index.ts +1 -4
  96. package/src/services/message/index.ts +1 -0
  97. package/src/services/topic/index.ts +16 -0
  98. package/src/store/serverConfig/selectors.ts +1 -0
@@ -0,0 +1,227 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { TopicShareModel } from '@/database/models/topicShare';
5
+
6
+ vi.mock('@/database/models/topicShare', () => ({
7
+ TopicShareModel: {
8
+ findByShareIdWithAccessCheck: vi.fn(),
9
+ incrementPageViewCount: vi.fn(),
10
+ },
11
+ }));
12
+
13
+ vi.mock('@/database/server', () => ({
14
+ getServerDB: vi.fn(),
15
+ }));
16
+
17
+ describe('shareRouter', () => {
18
+ describe('getSharedTopic', () => {
19
+ it('should return shared topic data for valid share', async () => {
20
+ const mockShare = {
21
+ agentAvatar: 'avatar.png',
22
+ agentBackgroundColor: '#fff',
23
+ agentId: 'agent-1',
24
+ agentMarketIdentifier: 'market-id',
25
+ agentSlug: 'agent-slug',
26
+ agentTitle: 'Test Agent',
27
+ groupAvatar: null,
28
+ groupBackgroundColor: null,
29
+ groupId: null,
30
+ groupMembers: undefined,
31
+ groupTitle: null,
32
+ ownerId: 'user-1',
33
+ shareId: 'share-123',
34
+ title: 'Test Topic',
35
+ topicId: 'topic-1',
36
+ visibility: 'link',
37
+ };
38
+
39
+ vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockResolvedValue(mockShare);
40
+ vi.mocked(TopicShareModel.incrementPageViewCount).mockResolvedValue(undefined);
41
+
42
+ const ctx = {
43
+ serverDB: {} as any,
44
+ userId: 'user-1',
45
+ };
46
+
47
+ const share = await TopicShareModel.findByShareIdWithAccessCheck(
48
+ ctx.serverDB,
49
+ 'share-123',
50
+ ctx.userId,
51
+ );
52
+
53
+ expect(share).toBeDefined();
54
+ expect(share.shareId).toBe('share-123');
55
+ expect(share.topicId).toBe('topic-1');
56
+ expect(share.title).toBe('Test Topic');
57
+ expect(share.visibility).toBe('link');
58
+
59
+ // Verify incrementPageViewCount would be called
60
+ await TopicShareModel.incrementPageViewCount(ctx.serverDB, 'share-123');
61
+ expect(TopicShareModel.incrementPageViewCount).toHaveBeenCalledWith(
62
+ ctx.serverDB,
63
+ 'share-123',
64
+ );
65
+ });
66
+
67
+ it('should return agent meta when share has agent', async () => {
68
+ const mockShare = {
69
+ agentAvatar: 'avatar.png',
70
+ agentBackgroundColor: '#ffffff',
71
+ agentId: 'agent-1',
72
+ agentMarketIdentifier: 'market-agent',
73
+ agentSlug: 'test-agent',
74
+ agentTitle: 'Test Agent Title',
75
+ groupAvatar: null,
76
+ groupBackgroundColor: null,
77
+ groupId: null,
78
+ groupMembers: undefined,
79
+ groupTitle: null,
80
+ ownerId: 'user-1',
81
+ shareId: 'share-123',
82
+ title: 'Topic with Agent',
83
+ topicId: 'topic-1',
84
+ visibility: 'link',
85
+ };
86
+
87
+ vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockResolvedValue(mockShare);
88
+
89
+ const ctx = {
90
+ serverDB: {} as any,
91
+ userId: null,
92
+ };
93
+
94
+ const share = await TopicShareModel.findByShareIdWithAccessCheck(
95
+ ctx.serverDB,
96
+ 'share-123',
97
+ undefined,
98
+ );
99
+
100
+ expect(share.agentId).toBe('agent-1');
101
+ expect(share.agentAvatar).toBe('avatar.png');
102
+ expect(share.agentTitle).toBe('Test Agent Title');
103
+ expect(share.agentMarketIdentifier).toBe('market-agent');
104
+ expect(share.agentSlug).toBe('test-agent');
105
+ });
106
+
107
+ it('should return group meta when share has group', async () => {
108
+ const mockShare = {
109
+ agentAvatar: null,
110
+ agentBackgroundColor: null,
111
+ agentId: null,
112
+ agentMarketIdentifier: null,
113
+ agentSlug: null,
114
+ agentTitle: null,
115
+ groupAvatar: 'group-avatar.png',
116
+ groupBackgroundColor: '#000000',
117
+ groupId: 'group-1',
118
+ groupMembers: [
119
+ { avatar: 'member1.png', backgroundColor: '#111' },
120
+ { avatar: 'member2.png', backgroundColor: '#222' },
121
+ ],
122
+ groupTitle: 'Test Group',
123
+ ownerId: 'user-1',
124
+ shareId: 'share-456',
125
+ title: 'Group Topic',
126
+ topicId: 'topic-2',
127
+ visibility: 'link',
128
+ };
129
+
130
+ vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockResolvedValue(mockShare);
131
+
132
+ const ctx = {
133
+ serverDB: {} as any,
134
+ userId: 'user-2',
135
+ };
136
+
137
+ const share = await TopicShareModel.findByShareIdWithAccessCheck(
138
+ ctx.serverDB,
139
+ 'share-456',
140
+ ctx.userId,
141
+ );
142
+
143
+ expect(share.groupId).toBe('group-1');
144
+ expect(share.groupTitle).toBe('Test Group');
145
+ expect(share.groupAvatar).toBe('group-avatar.png');
146
+ expect(share.groupMembers).toHaveLength(2);
147
+ });
148
+
149
+ it('should throw NOT_FOUND for non-existent share', async () => {
150
+ vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockRejectedValue(
151
+ new TRPCError({ code: 'NOT_FOUND', message: 'Share not found' }),
152
+ );
153
+
154
+ const ctx = {
155
+ serverDB: {} as any,
156
+ userId: 'user-1',
157
+ };
158
+
159
+ await expect(
160
+ TopicShareModel.findByShareIdWithAccessCheck(ctx.serverDB, 'non-existent', ctx.userId),
161
+ ).rejects.toThrow(TRPCError);
162
+ });
163
+
164
+ it('should throw FORBIDDEN for private share accessed by non-owner', async () => {
165
+ vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockRejectedValue(
166
+ new TRPCError({ code: 'FORBIDDEN', message: 'This share is private' }),
167
+ );
168
+
169
+ const ctx = {
170
+ serverDB: {} as any,
171
+ userId: 'other-user',
172
+ };
173
+
174
+ await expect(
175
+ TopicShareModel.findByShareIdWithAccessCheck(ctx.serverDB, 'private-share', ctx.userId),
176
+ ).rejects.toThrow(TRPCError);
177
+
178
+ try {
179
+ await TopicShareModel.findByShareIdWithAccessCheck(
180
+ ctx.serverDB,
181
+ 'private-share',
182
+ ctx.userId,
183
+ );
184
+ } catch (error) {
185
+ expect((error as TRPCError).code).toBe('FORBIDDEN');
186
+ }
187
+ });
188
+
189
+ it('should allow owner to access private share', async () => {
190
+ const mockShare = {
191
+ agentAvatar: null,
192
+ agentBackgroundColor: null,
193
+ agentId: null,
194
+ agentMarketIdentifier: null,
195
+ agentSlug: null,
196
+ agentTitle: null,
197
+ groupAvatar: null,
198
+ groupBackgroundColor: null,
199
+ groupId: null,
200
+ groupMembers: undefined,
201
+ groupTitle: null,
202
+ ownerId: 'owner-user',
203
+ shareId: 'private-share',
204
+ title: 'Private Topic',
205
+ topicId: 'topic-private',
206
+ visibility: 'private',
207
+ };
208
+
209
+ vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockResolvedValue(mockShare);
210
+
211
+ const ctx = {
212
+ serverDB: {} as any,
213
+ userId: 'owner-user',
214
+ };
215
+
216
+ const share = await TopicShareModel.findByShareIdWithAccessCheck(
217
+ ctx.serverDB,
218
+ 'private-share',
219
+ ctx.userId,
220
+ );
221
+
222
+ expect(share).toBeDefined();
223
+ expect(share.ownerId).toBe('owner-user');
224
+ expect(share.visibility).toBe('private');
225
+ });
226
+ });
227
+ });
@@ -1,11 +1,16 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { TopicModel } from '@/database/models/topic';
4
+ import { TopicShareModel } from '@/database/models/topicShare';
4
5
 
5
6
  vi.mock('@/database/models/topic', () => ({
6
7
  TopicModel: vi.fn(),
7
8
  }));
8
9
 
10
+ vi.mock('@/database/models/topicShare', () => ({
11
+ TopicShareModel: vi.fn(),
12
+ }));
13
+
9
14
  vi.mock('@/database/server', () => ({
10
15
  getServerDB: vi.fn(),
11
16
  }));
@@ -260,4 +265,173 @@ describe('topicRouter', () => {
260
265
  expect(result).toEqual([{ id: 'topic1', title: 'Test' }]);
261
266
  });
262
267
  });
268
+
269
+ describe('topic sharing', () => {
270
+ it('should handle enableSharing with default visibility', async () => {
271
+ const mockCreate = vi.fn().mockResolvedValue({
272
+ id: 'share-123',
273
+ topicId: 'topic1',
274
+ userId: 'user1',
275
+ visibility: 'private',
276
+ });
277
+
278
+ vi.mocked(TopicShareModel).mockImplementation(
279
+ () =>
280
+ ({
281
+ create: mockCreate,
282
+ }) as any,
283
+ );
284
+
285
+ const ctx = {
286
+ topicShareModel: new TopicShareModel({} as any, 'user1'),
287
+ };
288
+
289
+ const result = await ctx.topicShareModel.create('topic1');
290
+
291
+ expect(mockCreate).toHaveBeenCalledWith('topic1');
292
+ expect(result.id).toBe('share-123');
293
+ expect(result.visibility).toBe('private');
294
+ });
295
+
296
+ it('should handle enableSharing with link visibility', async () => {
297
+ const mockCreate = vi.fn().mockResolvedValue({
298
+ id: 'share-456',
299
+ topicId: 'topic1',
300
+ userId: 'user1',
301
+ visibility: 'link',
302
+ });
303
+
304
+ vi.mocked(TopicShareModel).mockImplementation(
305
+ () =>
306
+ ({
307
+ create: mockCreate,
308
+ }) as any,
309
+ );
310
+
311
+ const ctx = {
312
+ topicShareModel: new TopicShareModel({} as any, 'user1'),
313
+ };
314
+
315
+ const result = await ctx.topicShareModel.create('topic1', 'link');
316
+
317
+ expect(mockCreate).toHaveBeenCalledWith('topic1', 'link');
318
+ expect(result.visibility).toBe('link');
319
+ });
320
+
321
+ it('should handle disableSharing', async () => {
322
+ const mockDeleteByTopicId = vi.fn().mockResolvedValue(undefined);
323
+
324
+ vi.mocked(TopicShareModel).mockImplementation(
325
+ () =>
326
+ ({
327
+ deleteByTopicId: mockDeleteByTopicId,
328
+ }) as any,
329
+ );
330
+
331
+ const ctx = {
332
+ topicShareModel: new TopicShareModel({} as any, 'user1'),
333
+ };
334
+
335
+ await ctx.topicShareModel.deleteByTopicId('topic1');
336
+
337
+ expect(mockDeleteByTopicId).toHaveBeenCalledWith('topic1');
338
+ });
339
+
340
+ it('should handle updateShareVisibility', async () => {
341
+ const mockUpdateVisibility = vi.fn().mockResolvedValue({
342
+ id: 'share-123',
343
+ topicId: 'topic1',
344
+ visibility: 'link',
345
+ });
346
+
347
+ vi.mocked(TopicShareModel).mockImplementation(
348
+ () =>
349
+ ({
350
+ updateVisibility: mockUpdateVisibility,
351
+ }) as any,
352
+ );
353
+
354
+ const ctx = {
355
+ topicShareModel: new TopicShareModel({} as any, 'user1'),
356
+ };
357
+
358
+ const result = await ctx.topicShareModel.updateVisibility('topic1', 'link');
359
+
360
+ expect(mockUpdateVisibility).toHaveBeenCalledWith('topic1', 'link');
361
+ expect(result.visibility).toBe('link');
362
+ });
363
+
364
+ it('should handle getShareInfo', async () => {
365
+ const mockGetByTopicId = vi.fn().mockResolvedValue({
366
+ id: 'share-123',
367
+ topicId: 'topic1',
368
+ visibility: 'link',
369
+ });
370
+
371
+ vi.mocked(TopicShareModel).mockImplementation(
372
+ () =>
373
+ ({
374
+ getByTopicId: mockGetByTopicId,
375
+ }) as any,
376
+ );
377
+
378
+ const ctx = {
379
+ topicShareModel: new TopicShareModel({} as any, 'user1'),
380
+ };
381
+
382
+ const result = await ctx.topicShareModel.getByTopicId('topic1');
383
+
384
+ expect(mockGetByTopicId).toHaveBeenCalledWith('topic1');
385
+ expect(result).toEqual({
386
+ id: 'share-123',
387
+ topicId: 'topic1',
388
+ visibility: 'link',
389
+ });
390
+ });
391
+
392
+ it('should return null when getShareInfo for non-shared topic', async () => {
393
+ const mockGetByTopicId = vi.fn().mockResolvedValue(null);
394
+
395
+ vi.mocked(TopicShareModel).mockImplementation(
396
+ () =>
397
+ ({
398
+ getByTopicId: mockGetByTopicId,
399
+ }) as any,
400
+ );
401
+
402
+ const ctx = {
403
+ topicShareModel: new TopicShareModel({} as any, 'user1'),
404
+ };
405
+
406
+ const result = await ctx.topicShareModel.getByTopicId('non-shared-topic');
407
+
408
+ expect(mockGetByTopicId).toHaveBeenCalledWith('non-shared-topic');
409
+ expect(result).toBeNull();
410
+ });
411
+
412
+ it('should handle all visibility types', async () => {
413
+ const mockCreate = vi.fn();
414
+
415
+ vi.mocked(TopicShareModel).mockImplementation(
416
+ () =>
417
+ ({
418
+ create: mockCreate,
419
+ }) as any,
420
+ );
421
+
422
+ const ctx = {
423
+ topicShareModel: new TopicShareModel({} as any, 'user1'),
424
+ };
425
+
426
+ // Test private visibility
427
+ mockCreate.mockResolvedValueOnce({ visibility: 'private' });
428
+ await ctx.topicShareModel.create('topic1', 'private');
429
+ expect(mockCreate).toHaveBeenLastCalledWith('topic1', 'private');
430
+
431
+ // Test link visibility
432
+ mockCreate.mockResolvedValueOnce({ visibility: 'link' });
433
+ await ctx.topicShareModel.create('topic2', 'link');
434
+ expect(mockCreate).toHaveBeenLastCalledWith('topic2', 'link');
435
+ });
436
+ });
263
437
  });
@@ -37,6 +37,7 @@ import { ragEvalRouter } from './ragEval';
37
37
  import { searchRouter } from './search';
38
38
  import { sessionRouter } from './session';
39
39
  import { sessionGroupRouter } from './sessionGroup';
40
+ import { shareRouter } from './share';
40
41
  import { threadRouter } from './thread';
41
42
  import { topicRouter } from './topic';
42
43
  import { uploadRouter } from './upload';
@@ -77,6 +78,7 @@ export const lambdaRouter = router({
77
78
  search: searchRouter,
78
79
  session: sessionRouter,
79
80
  sessionGroup: sessionGroupRouter,
81
+ share: shareRouter,
80
82
  thread: threadRouter,
81
83
  topic: topicRouter,
82
84
  upload: uploadRouter,
@@ -4,10 +4,12 @@ import {
4
4
  UpdateMessagePluginSchema,
5
5
  UpdateMessageRAGParamsSchema,
6
6
  } from '@lobechat/types';
7
+ import { TRPCError } from '@trpc/server';
7
8
  import { z } from 'zod';
8
9
 
9
10
  import { MessageModel } from '@/database/models/message';
10
- import { authedProcedure, router } from '@/libs/trpc/lambda';
11
+ import { TopicShareModel } from '@/database/models/topicShare';
12
+ import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
11
13
  import { serverDatabase } from '@/libs/trpc/lambda/middleware';
12
14
  import { FileService } from '@/server/services/file';
13
15
  import { MessageService } from '@/server/services/message';
@@ -89,7 +91,8 @@ export const messageRouter = router({
89
91
  return ctx.messageModel.getHeatmaps();
90
92
  }),
91
93
 
92
- getMessages: messageProcedure
94
+ getMessages: publicProcedure
95
+ .use(serverDatabase)
93
96
  .input(
94
97
  z.object({
95
98
  agentId: z.string().nullable().optional(),
@@ -99,11 +102,41 @@ export const messageRouter = router({
99
102
  sessionId: z.string().nullable().optional(),
100
103
  threadId: z.string().nullable().optional(),
101
104
  topicId: z.string().nullable().optional(),
105
+ topicShareId: z.string().optional(),
102
106
  }),
103
107
  )
104
108
  .query(async ({ input, ctx }) => {
105
- return ctx.messageModel.query(input, {
106
- postProcessUrl: (path) => ctx.fileService.getFullFileUrl(path),
109
+ const { topicShareId, ...queryParams } = input;
110
+
111
+ // Public access via topicShareId
112
+ if (topicShareId) {
113
+ const share = await TopicShareModel.findByShareIdWithAccessCheck(
114
+ ctx.serverDB,
115
+ topicShareId,
116
+ ctx.userId ?? undefined,
117
+ );
118
+
119
+ const messageModel = new MessageModel(ctx.serverDB, share.ownerId);
120
+ const fileService = new FileService(ctx.serverDB, share.ownerId);
121
+
122
+ return messageModel.query(
123
+ { ...queryParams, topicId: share.topicId },
124
+ {
125
+ postProcessUrl: (path) => fileService.getFullFileUrl(path),
126
+ },
127
+ );
128
+ }
129
+
130
+ // Authenticated access - require userId
131
+ if (!ctx.userId) {
132
+ throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Authentication required' });
133
+ }
134
+
135
+ const messageModel = new MessageModel(ctx.serverDB, ctx.userId);
136
+ const fileService = new FileService(ctx.serverDB, ctx.userId);
137
+
138
+ return messageModel.query(queryParams, {
139
+ postProcessUrl: (path) => fileService.getFullFileUrl(path),
107
140
  });
108
141
  }),
109
142
 
@@ -0,0 +1,55 @@
1
+ import type { SharedTopicData } from '@lobechat/types';
2
+ import { z } from 'zod';
3
+
4
+ import { TopicShareModel } from '@/database/models/topicShare';
5
+ import { publicProcedure, router } from '@/libs/trpc/lambda';
6
+ import { serverDatabase } from '@/libs/trpc/lambda/middleware';
7
+
8
+ export const shareRouter = router({
9
+ /**
10
+ * Get shared topic metadata for public access
11
+ * Uses shareId (not topicId) for access
12
+ * Visibility check: owner can always access, others depend on visibility setting
13
+ */
14
+ getSharedTopic: publicProcedure
15
+ .use(serverDatabase)
16
+ .input(z.object({ shareId: z.string() }))
17
+ .query(async ({ input, ctx }): Promise<SharedTopicData> => {
18
+ const share = await TopicShareModel.findByShareIdWithAccessCheck(
19
+ ctx.serverDB,
20
+ input.shareId,
21
+ ctx.userId ?? undefined,
22
+ );
23
+
24
+ // Increment page view count after visibility check passes
25
+ await TopicShareModel.incrementPageViewCount(ctx.serverDB, input.shareId);
26
+
27
+ return {
28
+ agentId: share.agentId,
29
+ agentMeta: share.agentId
30
+ ? {
31
+ avatar: share.agentAvatar,
32
+ backgroundColor: share.agentBackgroundColor,
33
+ marketIdentifier: share.agentMarketIdentifier,
34
+ slug: share.agentSlug,
35
+ title: share.agentTitle,
36
+ }
37
+ : undefined,
38
+ groupId: share.groupId,
39
+ groupMeta: share.groupId
40
+ ? {
41
+ avatar: share.groupAvatar,
42
+ backgroundColor: share.groupBackgroundColor,
43
+ members: share.groupMembers,
44
+ title: share.groupTitle,
45
+ }
46
+ : undefined,
47
+ shareId: share.shareId,
48
+ title: share.title,
49
+ topicId: share.topicId,
50
+ visibility: share.visibility as SharedTopicData['visibility'],
51
+ };
52
+ }),
53
+ });
54
+
55
+ export type ShareRouter = typeof shareRouter;
@@ -8,6 +8,7 @@ import { after } from 'next/server';
8
8
  import { z } from 'zod';
9
9
 
10
10
  import { TopicModel } from '@/database/models/topic';
11
+ import { TopicShareModel } from '@/database/models/topicShare';
11
12
  import { AgentMigrationRepo } from '@/database/repositories/agentMigration';
12
13
  import { TopicImporterRepo } from '@/database/repositories/topicImporter';
13
14
  import { agents, chatGroups, chatGroupsAgents } from '@/database/schemas';
@@ -30,6 +31,7 @@ const topicProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
30
31
  agentMigrationRepo: new AgentMigrationRepo(ctx.serverDB, ctx.userId),
31
32
  topicImporterRepo: new TopicImporterRepo(ctx.serverDB, ctx.userId),
32
33
  topicModel: new TopicModel(ctx.serverDB, ctx.userId),
34
+ topicShareModel: new TopicShareModel(ctx.serverDB, ctx.userId),
33
35
  },
34
36
  });
35
37
  });
@@ -138,6 +140,29 @@ export const topicRouter = router({
138
140
  return data.id;
139
141
  }),
140
142
 
143
+ /**
144
+ * Disable sharing for a topic (deletes share record)
145
+ */
146
+ disableSharing: topicProcedure
147
+ .input(z.object({ topicId: z.string() }))
148
+ .mutation(async ({ input, ctx }) => {
149
+ return ctx.topicShareModel.deleteByTopicId(input.topicId);
150
+ }),
151
+
152
+ /**
153
+ * Enable sharing for a topic (creates share record)
154
+ */
155
+ enableSharing: topicProcedure
156
+ .input(
157
+ z.object({
158
+ topicId: z.string(),
159
+ visibility: z.enum(['private', 'link']).optional(),
160
+ }),
161
+ )
162
+ .mutation(async ({ input, ctx }) => {
163
+ return ctx.topicShareModel.create(input.topicId, input.visibility);
164
+ }),
165
+
141
166
  getAllTopics: topicProcedure.query(async ({ ctx }) => {
142
167
  return ctx.topicModel.queryAll();
143
168
  }),
@@ -148,6 +173,12 @@ export const topicRouter = router({
148
173
  return ctx.topicModel.getCronTopicsGroupedByCronJob(input.agentId);
149
174
  }),
150
175
 
176
+ getShareInfo: topicProcedure
177
+ .input(z.object({ topicId: z.string() }))
178
+ .query(async ({ input, ctx }) => {
179
+ return ctx.topicShareModel.getByTopicId(input.topicId);
180
+ }),
181
+
151
182
  getTopics: topicProcedure
152
183
  .input(
153
184
  z.object({
@@ -419,6 +450,20 @@ export const topicRouter = router({
419
450
  return ctx.topicModel.queryByKeyword(input.keywords, resolved.sessionId);
420
451
  }),
421
452
 
453
+ /**
454
+ * Update share visibility
455
+ */
456
+ updateShareVisibility: topicProcedure
457
+ .input(
458
+ z.object({
459
+ topicId: z.string(),
460
+ visibility: z.enum(['private', 'link']),
461
+ }),
462
+ )
463
+ .mutation(async ({ input, ctx }) => {
464
+ return ctx.topicShareModel.updateVisibility(input.topicId, input.visibility);
465
+ }),
466
+
422
467
  updateTopic: topicProcedure
423
468
  .input(
424
469
  z.object({
@@ -89,10 +89,7 @@ class ChatGroupService {
89
89
  * Batch create virtual agents and add them to an existing group.
90
90
  * This is more efficient than calling createAgentOnly multiple times.
91
91
  */
92
- batchCreateAgentsInGroup = (
93
- groupId: string,
94
- agents: GroupMemberConfig[],
95
- ) => {
92
+ batchCreateAgentsInGroup = (groupId: string, agents: GroupMemberConfig[]) => {
96
93
  return lambdaClient.group.batchCreateAgentsInGroup.mutate({
97
94
  agents: agents as Partial<AgentItem>[],
98
95
  groupId,
@@ -28,6 +28,7 @@ export interface MessageQueryContext {
28
28
  groupId?: string;
29
29
  threadId?: string | null;
30
30
  topicId?: string | null;
31
+ topicShareId?: string;
31
32
  }
32
33
 
33
34
  export class MessageService {
@@ -85,6 +85,22 @@ export class TopicService {
85
85
  return lambdaClient.topic.updateTopicMetadata.mutate({ id, metadata });
86
86
  };
87
87
 
88
+ getShareInfo = (topicId: string) => {
89
+ return lambdaClient.topic.getShareInfo.query({ topicId });
90
+ };
91
+
92
+ enableSharing = (topicId: string, visibility?: 'private' | 'link') => {
93
+ return lambdaClient.topic.enableSharing.mutate({ topicId, visibility });
94
+ };
95
+
96
+ updateShareVisibility = (topicId: string, visibility: 'private' | 'link') => {
97
+ return lambdaClient.topic.updateShareVisibility.mutate({ topicId, visibility });
98
+ };
99
+
100
+ disableSharing = (topicId: string) => {
101
+ return lambdaClient.topic.disableSharing.mutate({ topicId });
102
+ };
103
+
88
104
  removeTopic = (id: string) => {
89
105
  return lambdaClient.topic.removeTopic.mutate({ id });
90
106
  };
@@ -3,6 +3,7 @@ import { type ServerConfigStore } from './store';
3
3
  export const featureFlagsSelectors = (s: ServerConfigStore) => s.featureFlags;
4
4
 
5
5
  export const serverConfigSelectors = {
6
+ enableBusinessFeatures: (s: ServerConfigStore) => s.serverConfig.enableBusinessFeatures || false,
6
7
  enableEmailVerification: (s: ServerConfigStore) =>
7
8
  s.serverConfig.enableEmailVerification || false,
8
9
  enableKlavis: (s: ServerConfigStore) => s.serverConfig.enableKlavis || false,