Skip to content

Commit e77ccc7

Browse files
committed
feat: enhance chat response handling with external links and user info
(cherry picked from commit 312862a)
1 parent aaefa13 commit e77ccc7

File tree

13 files changed

+190
-73
lines changed

13 files changed

+190
-73
lines changed

packages/global/core/chat/type.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export type ResponseTagItemType = {
131131
totalQuoteList?: SearchDataResponseItemType[];
132132
llmModuleAccount?: number;
133133
historyPreviewLength?: number;
134+
externalLinkList?: { name: string; url: string }[];
134135
};
135136

136137
export type ChatItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & {

packages/global/core/workflow/runtime/type.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export type ChatDispatchProps = {
5151
isChildApp?: boolean;
5252
};
5353
runningUserInfo: {
54+
username: string;
55+
teamName: string;
56+
memberName: string;
57+
contact: string;
5458
teamId: string;
5559
tmbId: string;
5660
};

packages/service/core/workflow/dispatch/child/runApp.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import { authAppByTmbId } from '../../../../support/permission/app/auth';
2020
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
2121
import { getAppVersionById } from '../../../app/version/controller';
2222
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
23-
import { getUserChatInfoAndAuthTeamPoints } from '../../../../support/permission/auth/team';
23+
import {
24+
getUserChatInfoAndAuthTeamPoints,
25+
getRunningUserInfoByTmbId
26+
} from '../../../../support/permission/auth/team';
2427

2528
type Props = ModuleDispatchProps<{
2629
[NodeInputKeyEnum.userChatInput]: string;
@@ -147,6 +150,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
147150
tmbId: String(appData.tmbId),
148151
isChildApp: true
149152
},
153+
runningUserInfo: await getRunningUserInfoByTmbId(appData.tmbId),
150154
runtimeNodes,
151155
runtimeEdges,
152156
histories: chatHistories,

packages/service/core/workflow/dispatch/child/runTool.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
9090
systemVar: {
9191
user: {
9292
id: variables.userId,
93+
userId: runningUserInfo.username,
94+
rank: runningUserInfo.contact,
95+
membername: runningUserInfo.memberName,
96+
teamName: runningUserInfo.teamName,
9397
teamId: runningUserInfo.teamId,
9498
name: runningUserInfo.tmbId
9599
},

packages/service/support/permission/auth/team.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,33 @@ export async function getUserChatInfoAndAuthTeamPoints(tmbId: string) {
3030
}
3131
};
3232
}
33+
34+
export async function getRunningUserInfoByTmbId(tmbId: string) {
35+
if (tmbId) {
36+
const tmb = await MongoTeamMember.findById(tmbId, 'teamId name userId') // team_members name is the user's name
37+
.populate<{ team: TeamSchema; user: UserModelSchema }>([
38+
{
39+
path: 'team',
40+
select: 'name'
41+
},
42+
{
43+
path: 'user',
44+
select: 'username contact'
45+
}
46+
])
47+
.lean();
48+
49+
if (!tmb) return Promise.reject(TeamErrEnum.notUser);
50+
51+
return {
52+
username: tmb.user.username,
53+
teamName: tmb.team.name,
54+
memberName: tmb.name,
55+
contact: tmb.user.contact || '',
56+
teamId: tmb.teamId,
57+
tmbId: tmb._id
58+
};
59+
}
60+
61+
return Promise.reject(TeamErrEnum.notUser);
62+
}

projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ResponseTags.tsx

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ const ResponseTags = ({
4343
const {
4444
totalQuoteList: quoteList = [],
4545
llmModuleAccount = 0,
46-
historyPreviewLength = 0
46+
historyPreviewLength = 0,
47+
externalLinkList = []
4748
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]);
4849

4950
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
@@ -68,38 +69,53 @@ const ResponseTags = ({
6869
? quoteListRef.current.scrollHeight > (isPc ? 50 : 55)
6970
: true;
7071

71-
const sourceList = useMemo(() => {
72-
return Object.values(
73-
quoteList.reduce((acc: Record<string, SearchDataResponseItemType[]>, cur) => {
74-
if (!acc[cur.collectionId]) {
75-
acc[cur.collectionId] = [cur];
76-
}
77-
return acc;
78-
}, {})
79-
)
80-
.flat()
81-
.map((item) => ({
82-
sourceName: item.sourceName,
83-
sourceId: item.sourceId,
84-
icon: item.imageId
85-
? 'core/dataset/imageFill'
86-
: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
87-
collectionId: item.collectionId,
88-
datasetId: item.datasetId
89-
}));
72+
// Dataset citation render items
73+
const datasetCitationList = useMemo(() => {
74+
// Keep first item per collectionId and preserve first-seen order
75+
const firstByCollection = new Map<string, SearchDataResponseItemType>();
76+
quoteList.forEach((cur) => {
77+
if (!firstByCollection.has(cur.collectionId)) {
78+
firstByCollection.set(cur.collectionId, cur);
79+
}
80+
});
81+
return Array.from(firstByCollection.values()).map((item) => ({
82+
itemType: 'dataset' as const,
83+
sourceName: item.sourceName,
84+
sourceId: item.sourceId,
85+
icon: item.imageId
86+
? 'core/dataset/imageFill'
87+
: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
88+
collectionId: item.collectionId,
89+
datasetId: item.datasetId
90+
}));
9091
}, [quoteList]);
9192

92-
const notEmptyTags =
93-
quoteList.length > 0 ||
94-
(llmModuleAccount === 1 && notSharePage) ||
95-
(llmModuleAccount > 1 && notSharePage) ||
96-
(isPc && durationSeconds > 0) ||
97-
notSharePage;
93+
// Merge dataset citations and external link references for unified rendering
94+
type RenderCitationItem =
95+
| {
96+
itemType: 'dataset';
97+
sourceName: string;
98+
sourceId?: string;
99+
icon: string;
100+
collectionId: string;
101+
datasetId: string;
102+
}
103+
| { itemType: 'link'; name: string; url: string };
104+
105+
const citationRenderList: RenderCitationItem[] = useMemo(() => {
106+
const linkItems: RenderCitationItem[] = externalLinkList.map((r) => ({
107+
...r,
108+
itemType: 'link'
109+
}));
110+
return [...datasetCitationList, ...linkItems];
111+
}, [datasetCitationList, externalLinkList]);
112+
113+
const notEmptyTags = notSharePage || quoteList.length > 0 || (isPc && durationSeconds > 0);
98114

99115
return !showTags ? null : (
100116
<>
101117
{/* quote */}
102-
{sourceList.length > 0 && (
118+
{citationRenderList.length > 0 && (
103119
<>
104120
<Flex justifyContent={'space-between'} alignItems={'center'}>
105121
<Box width={'100%'}>
@@ -143,9 +159,16 @@ const ResponseTags = ({
143159
: {}
144160
}
145161
>
146-
{sourceList.map((item, index) => {
162+
{citationRenderList.map((item, index) => {
163+
const key = item.itemType === 'dataset' ? item.collectionId : `${item.url}-${index}`;
164+
const label = item.itemType === 'dataset' ? item.sourceName : item.name;
147165
return (
148-
<MyTooltip key={item.collectionId} label={t('common:core.chat.quote.Read Quote')}>
166+
<MyTooltip
167+
key={key}
168+
label={
169+
item.itemType === 'dataset' ? t('common:core.chat.quote.Read Quote') : item.url
170+
}
171+
>
149172
<Flex
150173
alignItems={'center'}
151174
fontSize={'xs'}
@@ -161,7 +184,16 @@ const ResponseTags = ({
161184
cursor={'pointer'}
162185
onClick={(e) => {
163186
e.stopPropagation();
164-
onOpenCiteModal(item);
187+
if (item.itemType === 'dataset') {
188+
onOpenCiteModal({
189+
collectionId: item.collectionId,
190+
sourceId: item.sourceId,
191+
sourceName: item.sourceName,
192+
datasetId: item.datasetId
193+
});
194+
} else {
195+
window.open(item.url, '_blank');
196+
}
165197
}}
166198
height={6}
167199
>
@@ -177,14 +209,16 @@ const ResponseTags = ({
177209
{index + 1}
178210
</Flex>
179211
<Flex px={1.5}>
180-
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
212+
{item.itemType === 'dataset' && (
213+
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
214+
)}
181215
<Box
182216
className="textEllipsis3"
183217
wordBreak={'break-all'}
184218
flex={'1 0 0'}
185219
fontSize={'mini'}
186220
>
187-
{item.sourceName}
221+
{label}
188222
</Box>
189223
</Flex>
190224
</Flex>

projects/app/src/global/core/chat/utils.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,60 @@ export const getFlatAppResponses = (res: ChatHistoryItemResType[]): ChatHistoryI
3333
};
3434
export function addStatisticalDataToHistoryItem(historyItem: ChatItemType) {
3535
if (historyItem.obj !== ChatRoleEnum.AI) return historyItem;
36-
if (historyItem.totalQuoteList !== undefined) return historyItem;
37-
if (!historyItem.responseData) return historyItem;
3836

3937
// Flat children
4038
const flatResData = getFlatAppResponses(historyItem.responseData || []);
4139

40+
const llmModuleAccount = flatResData.filter(isLLMNode).length;
41+
const totalQuoteList = flatResData
42+
.filter((item) => item.moduleType === FlowNodeTypeEnum.datasetSearchNode)
43+
.map((item) => item.quoteList)
44+
.flat()
45+
.filter(Boolean) as SearchDataResponseItemType[];
46+
const historyPreviewLength = flatResData.find(isLLMNode)?.historyPreview?.length;
47+
48+
// Extract external link references from final tool responses in responseData
49+
const externalLinkList = (() => {
50+
try {
51+
const refs: { name: string; url: string }[] = [];
52+
const dedupe = new Set<string>();
53+
// Also parse from flattened flow node responses (toolRes.referenceDocuments)
54+
flatResData.forEach((res: ChatHistoryItemResType) => {
55+
const docs = res?.toolRes?.referenceDocuments;
56+
if (docs) {
57+
docs.forEach((doc: { name: string; webUrl?: string; dingUrl?: string }) => {
58+
const baseName = doc?.name || '';
59+
const webUrl = doc?.webUrl;
60+
const dingUrl = doc?.dingUrl;
61+
const push = (name: string, url?: string) => {
62+
if (!url) return;
63+
const key = `${name}::${url}`;
64+
if (!dedupe.has(key)) {
65+
dedupe.add(key);
66+
refs.push({ name, url });
67+
}
68+
};
69+
if (baseName) {
70+
push(`[Web] ${baseName}`, webUrl);
71+
push(`[Dingding] ${baseName}`, dingUrl);
72+
} else {
73+
push('[Web]', webUrl);
74+
push('[Dingding]', dingUrl);
75+
}
76+
});
77+
}
78+
});
79+
return refs;
80+
} catch (e) {
81+
return [] as { name: string; url: string }[];
82+
}
83+
})();
84+
4285
return {
4386
...historyItem,
44-
llmModuleAccount: flatResData.filter(isLLMNode).length,
45-
totalQuoteList: flatResData
46-
.filter((item) => item.moduleType === FlowNodeTypeEnum.datasetSearchNode)
47-
.map((item) => item.quoteList)
48-
.flat()
49-
.filter(Boolean) as SearchDataResponseItemType[],
50-
historyPreviewLength: flatResData.find(isLLMNode)?.historyPreview?.length
87+
...(llmModuleAccount ? { llmModuleAccount } : {}),
88+
...(totalQuoteList.length ? { totalQuoteList } : {}),
89+
...(historyPreviewLength ? { historyPreviewLength } : {}),
90+
...(externalLinkList.length ? { externalLinkList } : {})
5191
};
5292
}

projects/app/src/pages/api/core/chat/chatTest.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'
1010
import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type';
1111
import { authApp } from '@fastgpt/service/support/permission/app/auth';
1212
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
13-
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
13+
import {
14+
getUserChatInfoAndAuthTeamPoints,
15+
getRunningUserInfoByTmbId
16+
} from '@fastgpt/service/support/permission/auth/team';
1417
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
1518
import {
1619
concatHistories,
@@ -184,10 +187,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
184187
teamId: app.teamId,
185188
tmbId: app.tmbId
186189
},
187-
runningUserInfo: {
188-
teamId,
189-
tmbId
190-
},
190+
runningUserInfo: await getRunningUserInfoByTmbId(tmbId),
191191

192192
chatId,
193193
responseChatItemId,

projects/app/src/pages/api/core/workflow/debug.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'
44
import { authApp } from '@fastgpt/service/support/permission/app/auth';
55
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
66
import { authCert } from '@fastgpt/service/support/permission/auth/common';
7-
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
7+
import {
8+
getUserChatInfoAndAuthTeamPoints,
9+
getRunningUserInfoByTmbId
10+
} from '@fastgpt/service/support/permission/auth/team';
811
import type { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
912
import { NextAPI } from '@/service/middleware/entry';
1013
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -63,10 +66,7 @@ async function handler(
6366
teamId: app.teamId,
6467
tmbId: app.tmbId
6568
},
66-
runningUserInfo: {
67-
teamId,
68-
tmbId
69-
},
69+
runningUserInfo: await getRunningUserInfoByTmbId(tmbId),
7070
runtimeNodes: nodes,
7171
runtimeEdges: edges,
7272
lastInteractive,

projects/app/src/pages/api/v1/chat/completions.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ import {
3434
removeEmptyUserInput
3535
} from '@fastgpt/global/core/chat/utils';
3636
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
37-
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
37+
import {
38+
getUserChatInfoAndAuthTeamPoints,
39+
getRunningUserInfoByTmbId
40+
} from '@fastgpt/service/support/permission/auth/team';
3841
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
3942
import { MongoApp } from '@fastgpt/service/core/app/schema';
4043
import { type AppSchema } from '@fastgpt/global/core/app/type';
@@ -286,10 +289,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
286289
teamId: String(app.teamId),
287290
tmbId: String(app.tmbId)
288291
},
289-
runningUserInfo: {
290-
teamId,
291-
tmbId
292-
},
292+
runningUserInfo: await getRunningUserInfoByTmbId(app.tmbId),
293293
uid: String(outLinkUserId || tmbId),
294294

295295
chatId,

0 commit comments

Comments
 (0)