@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.
- package/.cursor/rules/db-migrations.mdc +1 -1
- package/.cursor/rules/debug-usage.mdc +7 -5
- package/.cursor/rules/desktop-controller-tests.mdc +2 -1
- package/.cursor/rules/desktop-feature-implementation.mdc +9 -5
- package/.cursor/rules/desktop-local-tools-implement.mdc +67 -66
- package/.cursor/rules/desktop-menu-configuration.mdc +21 -9
- package/.cursor/rules/desktop-window-management.mdc +17 -2
- package/.cursor/rules/drizzle-schema-style-guide.mdc +6 -6
- package/.cursor/rules/hotkey.mdc +1 -0
- package/.cursor/rules/i18n.mdc +1 -0
- package/.cursor/rules/project-structure.mdc +16 -3
- package/.cursor/rules/react.mdc +17 -5
- package/.cursor/rules/recent-data-usage.mdc +2 -1
- package/.cursor/rules/testing-guide/testing-guide.mdc +262 -238
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +1 -1
- package/.cursor/rules/zustand-action-patterns.mdc +1 -1
- package/.cursor/rules/zustand-slice-organization.mdc +4 -4
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +1 -1
- package/GEMINI.md +1 -1
- package/changelog/v1.json +14 -0
- package/docs/development/database-schema.dbml +16 -0
- package/locales/en-US/chat.json +24 -0
- package/locales/en-US/setting.json +11 -0
- package/locales/zh-CN/chat.json +24 -0
- package/locales/zh-CN/setting.json +11 -0
- package/package.json +1 -1
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/BatchCreateAgents/index.tsx +2 -2
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/UpdateGroup/index.tsx +56 -56
- package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +3 -2
- package/packages/builtin-tool-group-agent-builder/src/executor.ts +2 -1
- package/packages/business/const/src/index.ts +3 -0
- package/packages/database/migrations/0069_add_topic_shares_table.sql +22 -0
- package/packages/database/migrations/meta/0069_snapshot.json +9704 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/models/__tests__/topicShare.test.ts +318 -0
- package/packages/database/src/models/topicShare.ts +177 -0
- package/packages/database/src/schemas/topic.ts +44 -2
- package/packages/types/src/agentCronJob/index.ts +19 -23
- package/packages/types/src/conversation.ts +5 -0
- package/packages/types/src/serverConfig.ts +1 -0
- package/packages/types/src/topic/topic.ts +46 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/Actions.tsx +31 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +10 -6
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/index.tsx +7 -11
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/useDropdownMenu.tsx +102 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +179 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +111 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobHeader.tsx +45 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +31 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +213 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +186 -344
- package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +24 -9
- package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/index.tsx +42 -97
- package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +4 -20
- package/src/app/[variants]/(main)/community/features/UserAvatar/index.tsx +15 -5
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/AgentProfilePopup.tsx +1 -6
- package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +26 -9
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/AspectRatioSelect/index.tsx +1 -2
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ImageNum.tsx +54 -173
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ResolutionSelect.tsx +22 -67
- package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +18 -0
- package/src/app/[variants]/router/desktopRouter.config.tsx +18 -0
- package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +54 -0
- package/src/app/[variants]/share/t/[id]/_layout/index.tsx +170 -0
- package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +66 -0
- package/src/app/[variants]/share/t/[id]/index.tsx +112 -0
- package/src/app/robots.tsx +1 -1
- package/src/business/client/BusinessMobileRoutes.tsx +1 -1
- package/src/features/Conversation/ChatList/index.tsx +12 -5
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +8 -4
- package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +15 -10
- package/src/features/Conversation/Messages/AssistantGroup/Tools.tsx +3 -1
- package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +3 -2
- package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +2 -2
- package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +25 -26
- package/src/features/Conversation/Messages/Supervisor/components/Group.tsx +4 -2
- package/src/features/Conversation/Messages/Tool/Tool/index.tsx +16 -12
- package/src/features/Conversation/Messages/Tool/index.tsx +20 -11
- package/src/features/Conversation/Messages/index.tsx +1 -1
- package/src/features/Conversation/store/slices/data/action.ts +2 -1
- package/src/features/SharePopover/index.tsx +215 -0
- package/src/features/SharePopover/style.ts +10 -0
- package/src/libs/next/proxy/define-config.ts +4 -1
- package/src/locales/default/chat.ts +26 -0
- package/src/proxy.ts +1 -0
- package/src/server/globalConfig/index.ts +1 -0
- package/src/server/routers/lambda/__tests__/message.test.ts +152 -0
- package/src/server/routers/lambda/__tests__/share.test.ts +227 -0
- package/src/server/routers/lambda/__tests__/topic.test.ts +174 -0
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/message.ts +37 -4
- package/src/server/routers/lambda/share.ts +55 -0
- package/src/server/routers/lambda/topic.ts +45 -0
- package/src/services/chatGroup/index.ts +1 -4
- package/src/services/message/index.ts +1 -0
- package/src/services/topic/index.ts +16 -0
- 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 {
|
|
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:
|
|
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
|
-
|
|
106
|
-
|
|
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,
|
|
@@ -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,
|