@lobehub/lobehub 2.0.0-next.277 → 2.0.0-next.279
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/zh-CN/chat.json +24 -0
- package/package.json +1 -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/conversation.ts +5 -0
- package/packages/types/src/topic/topic.ts +46 -0
- package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +24 -9
- package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +2 -1
- package/src/app/[variants]/(main)/agent/features/Portal/_layout/Mobile.tsx +3 -3
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +3 -2
- package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +26 -9
- package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +2 -1
- package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +3 -3
- package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -1
- 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 +17 -6
- 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.test.ts +42 -0
- package/src/features/Conversation/store/slices/data/action.ts +4 -2
- package/src/features/Portal/GroupThread/Header/index.tsx +2 -2
- package/src/features/Portal/MessageDetail/Body/index.tsx +3 -3
- package/src/features/Portal/components/Header.tsx +3 -3
- package/src/features/ProfileEditor/AgentTool.tsx +50 -19
- package/src/features/SharePopover/index.tsx +215 -0
- package/src/features/SharePopover/style.ts +10 -0
- package/src/hooks/useNavigateToAgent.ts +3 -3
- 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/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/message/index.ts +1 -0
- package/src/services/topic/index.ts +16 -0
- package/src/store/chat/slices/portal/action.test.ts +0 -41
- package/src/store/chat/slices/portal/action.ts +0 -25
- package/src/store/chat/slices/thread/action.test.ts +10 -6
- package/src/store/chat/slices/thread/action.ts +10 -3
- package/src/app/[variants]/(main)/group/features/Portal/features/Portal.tsx +0 -105
- package/src/app/[variants]/(main)/group/features/Portal/features/PortalPanel.tsx +0 -23
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button, Flexbox, Popover, copyToClipboard, usePopoverContext } from '@lobehub/ui';
|
|
4
|
+
import { App, Divider, Select, Skeleton, Typography } from 'antd';
|
|
5
|
+
import { CopyIcon, ExternalLinkIcon, LinkIcon, LockIcon } from 'lucide-react';
|
|
6
|
+
import { type ReactNode, memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import useSWR from 'swr';
|
|
9
|
+
|
|
10
|
+
import { useIsMobile } from '@/hooks/useIsMobile';
|
|
11
|
+
import { topicService } from '@/services/topic';
|
|
12
|
+
import { useChatStore } from '@/store/chat';
|
|
13
|
+
|
|
14
|
+
import { useStyles } from './style';
|
|
15
|
+
|
|
16
|
+
type Visibility = 'private' | 'link';
|
|
17
|
+
|
|
18
|
+
interface SharePopoverContentProps {
|
|
19
|
+
onOpenModal?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SharePopoverContent = memo<SharePopoverContentProps>(({ onOpenModal }) => {
|
|
23
|
+
const { t } = useTranslation('chat');
|
|
24
|
+
const { message, modal } = App.useApp();
|
|
25
|
+
const { styles } = useStyles();
|
|
26
|
+
const [updating, setUpdating] = useState(false);
|
|
27
|
+
const { close } = usePopoverContext();
|
|
28
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
|
|
30
|
+
const activeTopicId = useChatStore((s) => s.activeTopicId);
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
data: shareInfo,
|
|
34
|
+
isLoading,
|
|
35
|
+
mutate,
|
|
36
|
+
} = useSWR(
|
|
37
|
+
activeTopicId ? ['topic-share-info', activeTopicId] : null,
|
|
38
|
+
() => topicService.getShareInfo(activeTopicId!),
|
|
39
|
+
{ revalidateOnFocus: false },
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Auto-create share record if not exists
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!isLoading && !shareInfo && activeTopicId) {
|
|
45
|
+
topicService.enableSharing(activeTopicId, 'private').then(() => mutate());
|
|
46
|
+
}
|
|
47
|
+
}, [isLoading, shareInfo, activeTopicId, mutate]);
|
|
48
|
+
|
|
49
|
+
const shareUrl = shareInfo?.id ? `${window.location.origin}/share/t/${shareInfo.id}` : '';
|
|
50
|
+
const currentVisibility = (shareInfo?.visibility as Visibility) || 'private';
|
|
51
|
+
|
|
52
|
+
const updateVisibility = useCallback(
|
|
53
|
+
async (visibility: Visibility) => {
|
|
54
|
+
if (!activeTopicId) return;
|
|
55
|
+
|
|
56
|
+
setUpdating(true);
|
|
57
|
+
try {
|
|
58
|
+
await topicService.updateShareVisibility(activeTopicId, visibility);
|
|
59
|
+
await mutate();
|
|
60
|
+
message.success(t('shareModal.link.visibilityUpdated'));
|
|
61
|
+
} catch {
|
|
62
|
+
message.error(t('shareModal.link.updateError'));
|
|
63
|
+
} finally {
|
|
64
|
+
setUpdating(false);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[activeTopicId, mutate, message, t],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const handleVisibilityChange = useCallback(
|
|
71
|
+
(visibility: Visibility) => {
|
|
72
|
+
// Show confirmation when changing from private to link
|
|
73
|
+
if (currentVisibility === 'private' && visibility === 'link') {
|
|
74
|
+
modal.confirm({
|
|
75
|
+
cancelText: t('cancel', { ns: 'common' }),
|
|
76
|
+
content: t('shareModal.popover.privacyWarning.content'),
|
|
77
|
+
okText: t('shareModal.popover.privacyWarning.confirm'),
|
|
78
|
+
onOk: () => updateVisibility(visibility),
|
|
79
|
+
title: t('shareModal.popover.privacyWarning.title'),
|
|
80
|
+
type: 'warning',
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
updateVisibility(visibility);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
[currentVisibility, modal, t, updateVisibility],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const handleCopyLink = useCallback(async () => {
|
|
90
|
+
if (!shareUrl) return;
|
|
91
|
+
await copyToClipboard(shareUrl);
|
|
92
|
+
message.success(t('shareModal.copyLinkSuccess'));
|
|
93
|
+
}, [shareUrl, message, t]);
|
|
94
|
+
|
|
95
|
+
const handleOpenModal = useCallback(() => {
|
|
96
|
+
close();
|
|
97
|
+
onOpenModal?.();
|
|
98
|
+
}, [close, onOpenModal]);
|
|
99
|
+
|
|
100
|
+
// Loading state
|
|
101
|
+
if (isLoading || !shareInfo) {
|
|
102
|
+
return (
|
|
103
|
+
<Flexbox className={styles.container} gap={16}>
|
|
104
|
+
<Typography.Text strong>{t('share', { ns: 'common' })}</Typography.Text>
|
|
105
|
+
<Skeleton active paragraph={{ rows: 2 }} />
|
|
106
|
+
</Flexbox>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const visibilityOptions = [
|
|
111
|
+
{
|
|
112
|
+
icon: <LockIcon size={14} />,
|
|
113
|
+
label: t('shareModal.link.permissionPrivate'),
|
|
114
|
+
value: 'private',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
icon: <LinkIcon size={14} />,
|
|
118
|
+
label: t('shareModal.link.permissionLink'),
|
|
119
|
+
value: 'link',
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const getVisibilityHint = () => {
|
|
124
|
+
switch (currentVisibility) {
|
|
125
|
+
case 'private': {
|
|
126
|
+
return t('shareModal.link.privateHint');
|
|
127
|
+
}
|
|
128
|
+
case 'link': {
|
|
129
|
+
return t('shareModal.link.linkHint');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Flexbox className={styles.container} gap={12} ref={containerRef}>
|
|
136
|
+
<Typography.Text strong>{t('shareModal.popover.title')}</Typography.Text>
|
|
137
|
+
|
|
138
|
+
<Flexbox gap={4}>
|
|
139
|
+
<Typography.Text type="secondary">{t('shareModal.popover.visibility')}</Typography.Text>
|
|
140
|
+
<Select
|
|
141
|
+
disabled={updating}
|
|
142
|
+
getPopupContainer={() => containerRef.current || document.body}
|
|
143
|
+
labelRender={({ value }) => {
|
|
144
|
+
const option = visibilityOptions.find((o) => o.value === value);
|
|
145
|
+
return (
|
|
146
|
+
<Flexbox align="center" gap={8} horizontal>
|
|
147
|
+
{option?.icon}
|
|
148
|
+
{option?.label}
|
|
149
|
+
</Flexbox>
|
|
150
|
+
);
|
|
151
|
+
}}
|
|
152
|
+
onChange={handleVisibilityChange}
|
|
153
|
+
optionRender={(option) => (
|
|
154
|
+
<Flexbox align="center" gap={8} horizontal>
|
|
155
|
+
{visibilityOptions.find((o) => o.value === option.value)?.icon}
|
|
156
|
+
{option.label}
|
|
157
|
+
</Flexbox>
|
|
158
|
+
)}
|
|
159
|
+
options={visibilityOptions}
|
|
160
|
+
style={{ width: '100%' }}
|
|
161
|
+
value={currentVisibility}
|
|
162
|
+
/>
|
|
163
|
+
</Flexbox>
|
|
164
|
+
|
|
165
|
+
<Typography.Text className={styles.hint} type="secondary">
|
|
166
|
+
{getVisibilityHint()}
|
|
167
|
+
</Typography.Text>
|
|
168
|
+
|
|
169
|
+
<Divider style={{ margin: '4px 0' }} />
|
|
170
|
+
|
|
171
|
+
<Flexbox align="center" horizontal justify="space-between">
|
|
172
|
+
<Button
|
|
173
|
+
icon={ExternalLinkIcon}
|
|
174
|
+
onClick={handleOpenModal}
|
|
175
|
+
size="small"
|
|
176
|
+
type="text"
|
|
177
|
+
variant="text"
|
|
178
|
+
>
|
|
179
|
+
{t('shareModal.popover.moreOptions')}
|
|
180
|
+
</Button>
|
|
181
|
+
<Button icon={CopyIcon} onClick={handleCopyLink} size="small" type="primary">
|
|
182
|
+
{t('shareModal.copyLink')}
|
|
183
|
+
</Button>
|
|
184
|
+
</Flexbox>
|
|
185
|
+
</Flexbox>
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
interface SharePopoverProps {
|
|
190
|
+
children?: ReactNode;
|
|
191
|
+
onOpenModal?: () => void;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const SharePopover = memo<SharePopoverProps>(({ children, onOpenModal }) => {
|
|
195
|
+
const isMobile = useIsMobile();
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Popover
|
|
199
|
+
arrow={false}
|
|
200
|
+
content={<SharePopoverContent onOpenModal={onOpenModal} />}
|
|
201
|
+
placement={isMobile ? 'top' : 'bottomRight'}
|
|
202
|
+
styles={{
|
|
203
|
+
content: {
|
|
204
|
+
padding: 0,
|
|
205
|
+
width: isMobile ? '100vw' : 366,
|
|
206
|
+
},
|
|
207
|
+
}}
|
|
208
|
+
trigger={['click']}
|
|
209
|
+
>
|
|
210
|
+
{children}
|
|
211
|
+
</Popover>
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export default SharePopover;
|
|
@@ -5,15 +5,15 @@ import { useQueryRoute } from '@/hooks/useQueryRoute';
|
|
|
5
5
|
import { useChatStore } from '@/store/chat';
|
|
6
6
|
|
|
7
7
|
export const useNavigateToAgent = () => {
|
|
8
|
-
const
|
|
8
|
+
const clearPortalStack = useChatStore((s) => s.clearPortalStack);
|
|
9
9
|
const router = useQueryRoute();
|
|
10
10
|
|
|
11
11
|
return useCallback(
|
|
12
12
|
(agentId: string) => {
|
|
13
|
-
|
|
13
|
+
clearPortalStack();
|
|
14
14
|
|
|
15
15
|
router.push(SESSION_CHAT_URL(agentId, false));
|
|
16
16
|
},
|
|
17
|
-
[
|
|
17
|
+
[clearPortalStack, router],
|
|
18
18
|
);
|
|
19
19
|
};
|
|
@@ -8,7 +8,7 @@ import { auth } from '@/auth';
|
|
|
8
8
|
import { LOBE_LOCALE_COOKIE } from '@/const/locale';
|
|
9
9
|
import { isDesktop } from '@/const/version';
|
|
10
10
|
import { appEnv } from '@/envs/app';
|
|
11
|
-
import { OAUTH_AUTHORIZED
|
|
11
|
+
import { OAUTH_AUTHORIZED, authEnv } from '@/envs/auth';
|
|
12
12
|
import NextAuth from '@/libs/next-auth';
|
|
13
13
|
import { type Locales } from '@/locales/resources';
|
|
14
14
|
import { parseBrowserLanguage } from '@/utils/locale';
|
|
@@ -107,6 +107,7 @@ export function defineConfig() {
|
|
|
107
107
|
'/me',
|
|
108
108
|
'/desktop-onboarding',
|
|
109
109
|
'/onboarding',
|
|
110
|
+
'/share',
|
|
110
111
|
];
|
|
111
112
|
const isSpaRoute = spaRoutes.some((route) => url.pathname.startsWith(route));
|
|
112
113
|
|
|
@@ -184,6 +185,8 @@ export function defineConfig() {
|
|
|
184
185
|
'/oidc/token',
|
|
185
186
|
// market
|
|
186
187
|
'/market-auth-callback',
|
|
188
|
+
// public share pages
|
|
189
|
+
'/share(.*)',
|
|
187
190
|
]);
|
|
188
191
|
|
|
189
192
|
const isProtectedRoute = createRouteMatcher([
|
|
@@ -283,6 +283,8 @@ export default {
|
|
|
283
283
|
'sessionGroup.sorting': 'Group sorting updating...',
|
|
284
284
|
'sessionGroup.tooLong': 'Group name length should be between 1-20',
|
|
285
285
|
'shareModal.copy': 'Copy',
|
|
286
|
+
'shareModal.copyLink': 'Copy Link',
|
|
287
|
+
'shareModal.copyLinkSuccess': 'Link copied',
|
|
286
288
|
'shareModal.download': 'Download Screenshot',
|
|
287
289
|
'shareModal.downloadError': 'Download failed',
|
|
288
290
|
'shareModal.downloadFile': 'Download File',
|
|
@@ -298,12 +300,27 @@ export default {
|
|
|
298
300
|
'shareModal.imageType': 'Image Format',
|
|
299
301
|
'shareModal.includeTool': 'Include Skill messages',
|
|
300
302
|
'shareModal.includeUser': 'Include User Messages',
|
|
303
|
+
'shareModal.link': 'Link',
|
|
304
|
+
'shareModal.link.linkHint': 'Anyone with the link can view this topic',
|
|
305
|
+
'shareModal.link.noTopic': 'Start a conversation first to share',
|
|
306
|
+
'shareModal.link.permissionLink': 'Anyone with the link',
|
|
307
|
+
'shareModal.link.permissionPrivate': 'Private',
|
|
308
|
+
'shareModal.link.privateHint': 'Only you can access this link',
|
|
309
|
+
'shareModal.link.updateError': 'Failed to update sharing settings',
|
|
310
|
+
'shareModal.link.visibilityUpdated': 'Visibility updated',
|
|
301
311
|
'shareModal.loadingPdf': 'Loading PDF...',
|
|
302
312
|
'shareModal.noPdfData': 'No PDF data available',
|
|
303
313
|
'shareModal.pdf': 'PDF',
|
|
304
314
|
'shareModal.pdfErrorDescription': 'An error occurred while generating the PDF, please try again',
|
|
305
315
|
'shareModal.pdfGenerationError': 'PDF generation failed',
|
|
306
316
|
'shareModal.pdfReady': 'PDF is ready',
|
|
317
|
+
'shareModal.popover.moreOptions': 'More share options',
|
|
318
|
+
'shareModal.popover.privacyWarning.confirm': 'I understand, continue',
|
|
319
|
+
'shareModal.popover.privacyWarning.content':
|
|
320
|
+
'Please ensure the conversation does not contain any private or sensitive information before sharing. LobeHub is not responsible for any security issues that may arise from sharing.',
|
|
321
|
+
'shareModal.popover.privacyWarning.title': 'Privacy Notice',
|
|
322
|
+
'shareModal.popover.title': 'Share Topic',
|
|
323
|
+
'shareModal.popover.visibility': 'Visibility',
|
|
307
324
|
'shareModal.regeneratePdf': 'Regenerate PDF',
|
|
308
325
|
'shareModal.screenshot': 'Screenshot',
|
|
309
326
|
'shareModal.settings': 'Export Settings',
|
|
@@ -316,6 +333,15 @@ export default {
|
|
|
316
333
|
'shareModal.withPluginInfo': 'Include Skill Information',
|
|
317
334
|
'shareModal.withRole': 'Include Message Role',
|
|
318
335
|
'shareModal.withSystemRole': 'Include Agent Profile',
|
|
336
|
+
'sharePage.error.forbidden.subtitle': 'This share is private and not accessible.',
|
|
337
|
+
'sharePage.error.forbidden.title': 'Access Denied',
|
|
338
|
+
'sharePage.error.notFound.subtitle': 'This topic does not exist or has been removed.',
|
|
339
|
+
'sharePage.error.notFound.title': 'Topic Not Found',
|
|
340
|
+
'sharePage.error.unauthorized.action': 'Sign In',
|
|
341
|
+
'sharePage.error.unauthorized.subtitle': 'Please sign in to view this shared topic.',
|
|
342
|
+
'sharePage.error.unauthorized.title': 'Sign In Required',
|
|
343
|
+
'sharePageDisclaimer':
|
|
344
|
+
'This content is shared by a user and does not represent the views of LobeHub. LobeHub is not responsible for any consequences arising from this shared content.',
|
|
319
345
|
'stt.action': 'Voice Input',
|
|
320
346
|
'stt.loading': 'Recognizing...',
|
|
321
347
|
'stt.prettifying': 'Polishing...',
|
package/src/proxy.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { CreateMessageParams, UIChatMessage, UpdateMessageRAGParams } from '@lobechat/types';
|
|
2
|
+
import { TRPCError } from '@trpc/server';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
|
|
4
5
|
import { MessageModel } from '@/database/models/message';
|
|
6
|
+
import { TopicShareModel } from '@/database/models/topicShare';
|
|
5
7
|
import { FileService } from '@/server/services/file';
|
|
6
8
|
|
|
7
9
|
vi.mock('@/database/models/message', () => ({
|
|
8
10
|
MessageModel: vi.fn(),
|
|
9
11
|
}));
|
|
10
12
|
|
|
13
|
+
vi.mock('@/database/models/topicShare', () => ({
|
|
14
|
+
TopicShareModel: {
|
|
15
|
+
findByShareIdWithAccessCheck: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
11
19
|
vi.mock('@/server/services/file', () => ({
|
|
12
20
|
FileService: vi.fn(),
|
|
13
21
|
}));
|
|
@@ -345,4 +353,148 @@ describe('messageRouter', () => {
|
|
|
345
353
|
expect(result.rowCount).toBe(5);
|
|
346
354
|
});
|
|
347
355
|
});
|
|
356
|
+
|
|
357
|
+
describe('topicShareId support', () => {
|
|
358
|
+
it('should get messages via topicShareId for link share', async () => {
|
|
359
|
+
const mockShare = {
|
|
360
|
+
visibility: 'link',
|
|
361
|
+
ownerId: 'owner-user',
|
|
362
|
+
shareId: 'share-123',
|
|
363
|
+
topicId: 'topic-1',
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const mockMessages = [
|
|
367
|
+
{ id: 'msg1', content: 'Hello', role: 'user' },
|
|
368
|
+
{ id: 'msg2', content: 'Hi there', role: 'assistant' },
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
const mockQuery = vi.fn().mockResolvedValue(mockMessages);
|
|
372
|
+
const mockGetFullFileUrl = vi
|
|
373
|
+
.fn()
|
|
374
|
+
.mockImplementation((path: string) => `https://cdn/${path}`);
|
|
375
|
+
|
|
376
|
+
vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockResolvedValue(mockShare as any);
|
|
377
|
+
vi.mocked(MessageModel).mockImplementation(
|
|
378
|
+
() =>
|
|
379
|
+
({
|
|
380
|
+
query: mockQuery,
|
|
381
|
+
}) as any,
|
|
382
|
+
);
|
|
383
|
+
vi.mocked(FileService).mockImplementation(
|
|
384
|
+
() =>
|
|
385
|
+
({
|
|
386
|
+
getFullFileUrl: mockGetFullFileUrl,
|
|
387
|
+
}) as any,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Simulate the router logic
|
|
391
|
+
const share = await TopicShareModel.findByShareIdWithAccessCheck(
|
|
392
|
+
{} as any,
|
|
393
|
+
'share-123',
|
|
394
|
+
undefined,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
expect(share).toBeDefined();
|
|
398
|
+
expect(share.topicId).toBe('topic-1');
|
|
399
|
+
expect(share.ownerId).toBe('owner-user');
|
|
400
|
+
|
|
401
|
+
// Create model using owner's id
|
|
402
|
+
const messageModel = new MessageModel({} as any, share.ownerId);
|
|
403
|
+
const result = await messageModel.query(
|
|
404
|
+
{ topicId: share.topicId },
|
|
405
|
+
{ postProcessUrl: mockGetFullFileUrl },
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(result).toEqual(mockMessages);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should allow owner to access private share messages', async () => {
|
|
412
|
+
const mockShare = {
|
|
413
|
+
visibility: 'private',
|
|
414
|
+
ownerId: 'owner-user',
|
|
415
|
+
shareId: 'private-share',
|
|
416
|
+
topicId: 'topic-private',
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockResolvedValue(mockShare as any);
|
|
420
|
+
|
|
421
|
+
const share = await TopicShareModel.findByShareIdWithAccessCheck(
|
|
422
|
+
{} as any,
|
|
423
|
+
'private-share',
|
|
424
|
+
'owner-user', // Owner accessing
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
expect(share).toBeDefined();
|
|
428
|
+
expect(share.visibility).toBe('private');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should throw FORBIDDEN for private share accessed by non-owner', async () => {
|
|
432
|
+
vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockRejectedValue(
|
|
433
|
+
new TRPCError({ code: 'FORBIDDEN', message: 'This share is private' }),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
await expect(
|
|
437
|
+
TopicShareModel.findByShareIdWithAccessCheck({} as any, 'private-share', 'other-user'),
|
|
438
|
+
).rejects.toThrow(TRPCError);
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await TopicShareModel.findByShareIdWithAccessCheck(
|
|
442
|
+
{} as any,
|
|
443
|
+
'private-share',
|
|
444
|
+
'other-user',
|
|
445
|
+
);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
expect((error as TRPCError).code).toBe('FORBIDDEN');
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should throw NOT_FOUND for non-existent share', async () => {
|
|
452
|
+
vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockRejectedValue(
|
|
453
|
+
new TRPCError({ code: 'NOT_FOUND', message: 'Share not found' }),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
await expect(
|
|
457
|
+
TopicShareModel.findByShareIdWithAccessCheck({} as any, 'non-existent', 'user1'),
|
|
458
|
+
).rejects.toThrow(TRPCError);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
await TopicShareModel.findByShareIdWithAccessCheck({} as any, 'non-existent', 'user1');
|
|
462
|
+
} catch (error) {
|
|
463
|
+
expect((error as TRPCError).code).toBe('NOT_FOUND');
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should use owner id to query messages for shared topic', async () => {
|
|
468
|
+
const mockShare = {
|
|
469
|
+
visibility: 'link',
|
|
470
|
+
ownerId: 'topic-owner',
|
|
471
|
+
shareId: 'share-abc',
|
|
472
|
+
topicId: 'shared-topic',
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const mockQuery = vi.fn().mockResolvedValue([{ id: 'msg1' }]);
|
|
476
|
+
|
|
477
|
+
vi.mocked(TopicShareModel.findByShareIdWithAccessCheck).mockResolvedValue(mockShare as any);
|
|
478
|
+
vi.mocked(MessageModel).mockImplementation(
|
|
479
|
+
() =>
|
|
480
|
+
({
|
|
481
|
+
query: mockQuery,
|
|
482
|
+
}) as any,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const share = await TopicShareModel.findByShareIdWithAccessCheck(
|
|
486
|
+
{} as any,
|
|
487
|
+
'share-abc',
|
|
488
|
+
undefined,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// Verify we use the owner's id to create MessageModel
|
|
492
|
+
const messageModel = new MessageModel({} as any, share.ownerId);
|
|
493
|
+
await messageModel.query({ topicId: share.topicId }, {});
|
|
494
|
+
|
|
495
|
+
// Verify MessageModel was instantiated with owner's id
|
|
496
|
+
expect(MessageModel).toHaveBeenCalledWith({} as any, 'topic-owner');
|
|
497
|
+
expect(mockQuery).toHaveBeenCalledWith({ topicId: 'shared-topic' }, {});
|
|
498
|
+
});
|
|
499
|
+
});
|
|
348
500
|
});
|