Files
FastGPT/packages/service/core/chat/saveChat.ts
Archer 2ed1545eb5 V4.12.4 features (#5626)
* fix: push again, user select option button and form input radio content overflow (#5601)

* fix: push again, user select option button and form input radio content overflow

* fix: use useCallback instead of useMemo, fix unnecessary delete

* fix: Move the variable inside the component

* fix: do not pass valueLabel to MySelect

* ui

* del collection api adapt

* refactor: inherit permission (#5529)

* refactor: permission update conflict check function

* refactor(permission): app collaborator update api

* refactor(permission): support app update collaborator

* feat: support fe permission conflict check

* refactor(permission): app permission

* refactor(permission): dataset permission

* refactor(permission): team permission

* chore: fe adjust

* fix: type error

* fix: audit pagiation

* fix: tc

* chore: initv4130

* fix: app/dataset auth logic

* chore: move code

* refactor(permission): remove selfPermission

* fix: mock

* fix: test

* fix: app & dataset auth

* fix: inherit

* test(inheritPermission): test syncChildrenPermission

* prompt editor add list plugin (#5620)

* perf: search result (#5608)

* fix: table size (#5598)

* temp: list value

* backspace

* optimize code

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* fix: fe & member list (#5619)

* chore: initv4130

* fix: MemberItemCard

* fix: MemberItemCard

* chore: fe adjust & init script

* perf: test code

* doc

* fix debug variables (#5617)

* perf: search result (#5608)

* fix: table size (#5598)

* fix debug variables

* fix

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* perf: member ui

* fix: inherit bug (#5624)

* refactor(permission): remove getClbsWithInfo, which is useless

* fix: app list privateApp

* fix: get infos

* perf(fe): remove delete icon when it is disable in MemberItemCard

* fix: dataset private dataset

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* perf: auto coupon

* chore: upgrade script & get infos avatar  (#5625)

* fix: get infos

* chore: initv4130

* feat: support WecomRobot publish, and fix AesKey can not save bug (#5526)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* remove test

* rename init shell

* feat: collection page store

* reload sandbox

* pysandbox

* remove log

* chore: remove useless code (#5629)

* chore: remove useless code

* fix: checkConflict

* perf: support hidden type for RoleList

* fix: copy node

* update doc

* fix(permission): some bug (#5632)

* fix: app/dataset list

* fix: inherit bug

* perf: del app;i18n;save chat

* fix: test

* i18n

* fix: sumper overflow return OwnerRoleVal (#5633)

* remove invalid code

* fix: scroll

* fix: objectId

* update next

* update package

* object id

* mock redis

* feat: add redis append to resolve wecom stream response  (#5643)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

* feat: add redis append method

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* cache per

* fix(test): init team sub when creating mocked user (#5646)

* fix: button is not vertically centered (#5647)

* doc

* fix: gridFs objectId (#5649)

---------

Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>
2025-09-15 20:02:54 +08:00

345 lines
9.1 KiB
TypeScript

import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d';
import { MongoApp } from '../app/schema';
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { MongoChatItem } from './chatItemSchema';
import { MongoChat } from './chatSchema';
import { addLog } from '../../common/system/log';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { type StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { type AppChatConfigType } from '@fastgpt/global/core/app/type';
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { pushChatLog } from './pushChatLog';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { MongoAppChatLog } from '../app/logs/chatLogsSchema';
import { writePrimary } from '../../common/mongo/utils';
type Props = {
chatId: string;
appId: string;
teamId: string;
tmbId: string;
nodes: StoreNodeItemType[];
appChatConfig?: AppChatConfigType;
variables?: Record<string, any>;
isUpdateUseTime: boolean;
newTitle: string;
source: `${ChatSourceEnum}`;
sourceName?: string;
shareId?: string;
outLinkUid?: string;
content: [UserChatItemType & { dataId?: string }, AIChatItemType & { dataId?: string }];
metadata?: Record<string, any>;
durationSeconds: number; //s
errorMsg?: string;
};
export async function saveChat({
chatId,
appId,
teamId,
tmbId,
nodes,
appChatConfig,
variables,
isUpdateUseTime,
newTitle,
source,
sourceName,
shareId,
outLinkUid,
content,
durationSeconds,
errorMsg,
metadata = {}
}: Props) {
if (!chatId || chatId === 'NO_RECORD_HISTORIES') return;
try {
const chat = await MongoChat.findOne(
{
appId,
chatId
},
'_id metadata'
);
const metadataUpdate = {
...chat?.metadata,
...metadata
};
const { welcomeText, variables: variableList } = getAppChatConfig({
chatConfig: appChatConfig,
systemConfigNode: getGuideModule(nodes),
isPublicFetch: false
});
const pluginInputs = nodes?.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput
)?.inputs;
// Format save chat content: Remove quote q/a
const processedContent = content.map((item) => {
if (item.obj === ChatRoleEnum.AI) {
const nodeResponse = item[DispatchNodeResponseKeyEnum.nodeResponse]?.map((responseItem) => {
if (
responseItem.moduleType === FlowNodeTypeEnum.datasetSearchNode &&
responseItem.quoteList
) {
return {
...responseItem,
quoteList: responseItem.quoteList.map((quote: any) => ({
id: quote.id,
chunkIndex: quote.chunkIndex,
datasetId: quote.datasetId,
collectionId: quote.collectionId,
sourceId: quote.sourceId,
sourceName: quote.sourceName,
score: quote.score,
tokens: quote.tokens
}))
};
}
return responseItem;
});
return {
...item,
[DispatchNodeResponseKeyEnum.nodeResponse]: nodeResponse,
durationSeconds,
errorMsg
};
}
return item;
});
await mongoSessionRun(async (session) => {
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.create(
processedContent.map((item) => ({
chatId,
teamId,
tmbId,
appId,
...item
})),
{ session, ordered: true, ...writePrimary }
);
await MongoChat.updateOne(
{
appId,
chatId
},
{
$set: {
teamId,
tmbId,
appId,
chatId,
variableList,
welcomeText,
variables: variables || {},
pluginInputs,
title: newTitle,
source,
sourceName,
shareId,
outLinkUid,
metadata: metadataUpdate,
updateTime: new Date()
}
},
{
session,
upsert: true,
...writePrimary
}
);
pushChatLog({
chatId,
chatItemIdHuman: String(chatItemIdHuman),
chatItemIdAi: String(chatItemIdAi),
appId
});
});
try {
const userId = String(outLinkUid || tmbId);
const now = new Date();
const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000);
const aiResponse = processedContent.find((item) => item.obj === ChatRoleEnum.AI);
const errorCount = aiResponse?.responseData?.some((item) => item.errorText) ? 1 : 0;
const totalPoints =
aiResponse?.responseData?.reduce(
(sum: number, item: any) => sum + (item.totalPoints || 0),
0
) || 0;
const hasHistoryChat = await MongoAppChatLog.exists({
teamId,
appId,
userId,
createTime: { $lt: now }
});
await MongoAppChatLog.updateOne(
{
teamId,
appId,
chatId,
updateTime: { $gte: fifteenMinutesAgo }
},
{
$inc: {
chatItemCount: 1,
errorCount,
totalPoints,
totalResponseTime: durationSeconds
},
$set: {
updateTime: now,
sourceName
},
$setOnInsert: {
appId,
teamId,
chatId,
userId,
source,
createTime: now,
goodFeedbackCount: 0,
badFeedbackCount: 0,
isFirstChat: !hasHistoryChat
}
},
{
upsert: true,
...writePrimary
}
);
} catch (error) {
addLog.error('update chat log error', error);
}
if (isUpdateUseTime) {
await MongoApp.updateOne(
{ _id: appId },
{
updateTime: new Date()
},
{
...writePrimary
}
).catch();
}
} catch (error) {
addLog.error(`update chat history error`, error);
}
}
export const updateInteractiveChat = async ({
chatId,
appId,
userInteractiveVal,
aiResponse,
newVariables,
durationSeconds
}: {
chatId: string;
appId: string;
userInteractiveVal: string;
aiResponse: AIChatItemType & { dataId?: string };
newVariables?: Record<string, any>;
durationSeconds: number;
}) => {
if (!chatId) return;
const chatItem = await MongoChatItem.findOne({ appId, chatId, obj: ChatRoleEnum.AI }).sort({
_id: -1
});
if (!chatItem || chatItem.obj !== ChatRoleEnum.AI) return;
// Update interactive value
const interactiveValue = chatItem.value[chatItem.value.length - 1];
if (
!interactiveValue ||
interactiveValue.type !== ChatItemValueTypeEnum.interactive ||
!interactiveValue.interactive?.params
) {
return;
}
const parsedUserInteractiveVal = (() => {
try {
return JSON.parse(userInteractiveVal);
} catch (err) {
return userInteractiveVal;
}
})();
let finalInteractive = extractDeepestInteractive(interactiveValue.interactive);
if (finalInteractive.type === 'userSelect') {
finalInteractive.params.userSelectedVal = userInteractiveVal;
} else if (
finalInteractive.type === 'userInput' &&
typeof parsedUserInteractiveVal === 'object'
) {
finalInteractive.params.inputForm = finalInteractive.params.inputForm.map((item) => {
const itemValue = parsedUserInteractiveVal[item.label];
return itemValue !== undefined
? {
...item,
value: itemValue
}
: item;
});
finalInteractive.params.submitted = true;
}
if (aiResponse.customFeedbacks) {
chatItem.customFeedbacks = chatItem.customFeedbacks
? [...chatItem.customFeedbacks, ...aiResponse.customFeedbacks]
: aiResponse.customFeedbacks;
}
if (aiResponse.responseData) {
chatItem.responseData = chatItem.responseData
? mergeChatResponseData([...chatItem.responseData, ...aiResponse.responseData])
: aiResponse.responseData;
}
if (aiResponse.value) {
chatItem.value = chatItem.value ? [...chatItem.value, ...aiResponse.value] : aiResponse.value;
}
chatItem.durationSeconds = chatItem.durationSeconds
? +(chatItem.durationSeconds + durationSeconds).toFixed(2)
: durationSeconds;
await mongoSessionRun(async (session) => {
await chatItem.save({ session });
await MongoChat.updateOne(
{
appId,
chatId
},
{
$set: {
variables: newVariables,
updateTime: new Date()
}
},
{
session
}
);
});
};