diff --git a/README.md b/README.md index 0fe31e64aeaa..7f5d784385c1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 日语

-FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景! +FastGPT 是一个 AI Agent 构建平台,提供开箱即用的数据处理、模型调用等能力,同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的应用场景! diff --git a/docSite/content/zh-cn/docs/development/upgrading/495.md b/docSite/content/zh-cn/docs/development/upgrading/495.md index a862ac79265e..5548ff014528 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/495.md +++ b/docSite/content/zh-cn/docs/development/upgrading/495.md @@ -11,10 +11,16 @@ weight: 795 ## 🚀 新增内容 1. 团队成员权限细分,可分别控制是否可创建在根目录应用/知识库以及 API Key +2. 支持交互节点在嵌套工作流中使用。 ## ⚙️ 优化 +1. 繁体中文翻译。 + ## 🐛 修复 -1. password 检测规则错误 \ No newline at end of file +1. password 检测规则错误。 +2. 分享链接无法隐藏知识库检索结果。 +3. IOS 低版本正则兼容问题。 +4. 修复问答提取队列错误后,计数器未清零问题,导致问答提取队列失效。 \ No newline at end of file diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index c750ca2cd984..39058da5a67c 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -23,7 +23,7 @@ import { WorkflowResponseType } from '../../../../service/core/workflow/dispatch import { AiChatQuoteRoleType } from '../template/system/aiChat/type'; import { LafAccountType, OpenaiAccountType } from '../../../support/user/team/type'; import { CompletionFinishReason } from '../../ai/type'; - +import { WorkflowInteractiveResponseType } from '../template/system/interactive/type'; export type ExternalProviderType = { openaiAccount?: OpenaiAccountType; externalWorkflowVariables?: Record; @@ -55,6 +55,7 @@ export type ChatDispatchProps = { variables: Record; // global variable query: UserChatItemValueItemType[]; // trigger query chatConfig: AppSchema['chatConfig']; + lastInteractive?: WorkflowInteractiveResponseType; // last interactive response stream: boolean; maxRunTimes: number; isToolCall?: boolean; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 09f1e0c81138..403b526e4728 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -10,7 +10,19 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io'; import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants'; import { replaceVariable, valToStr } from '../../../common/string/tools'; - +import { + InteractiveNodeResponseType, + WorkflowInteractiveResponseType +} from '../template/system/interactive/type'; + +export const extractDeepestInteractive = ( + interactive: WorkflowInteractiveResponseType +): WorkflowInteractiveResponseType => { + if (interactive?.type === 'childrenInteractive' && interactive.params?.childrenResponse) { + return extractDeepestInteractive(interactive.params.childrenResponse); + } + return interactive; +}; export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => { let limit = 10; nodes.forEach((node) => { @@ -34,7 +46,9 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number 1. Get the interactive data 2. Check that the workflow starts at the interaction node */ -export const getLastInteractiveValue = (histories: ChatItemType[]) => { +export const getLastInteractiveValue = ( + histories: ChatItemType[] +): WorkflowInteractiveResponseType | undefined => { const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI); if (lastAIMessage) { @@ -45,7 +59,11 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => { lastValue.type !== ChatItemValueTypeEnum.interactive || !lastValue.interactive ) { - return null; + return; + } + + if (lastValue.interactive.type === 'childrenInteractive') { + return lastValue.interactive; } // Check is user select @@ -62,38 +80,29 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => { } } - return null; + return; }; export const initWorkflowEdgeStatus = ( - edges: StoreEdgeItemType[] | RuntimeEdgeItemType[], - histories?: ChatItemType[] + edges: StoreEdgeItemType[], + lastInteractive?: WorkflowInteractiveResponseType ): RuntimeEdgeItemType[] => { - // If there is a history, use the last interactive value - if (histories && histories.length > 0) { - const memoryEdges = getLastInteractiveValue(histories)?.memoryEdges; - + if (lastInteractive) { + const memoryEdges = lastInteractive.memoryEdges || []; if (memoryEdges && memoryEdges.length > 0) { return memoryEdges; } } - return ( - edges?.map((edge) => ({ - ...edge, - status: 'waiting' - })) || [] - ); + return edges?.map((edge) => ({ ...edge, status: 'waiting' })) || []; }; export const getWorkflowEntryNodeIds = ( nodes: (StoreNodeItemType | RuntimeNodeItemType)[], - histories?: ChatItemType[] + lastInteractive?: WorkflowInteractiveResponseType ) => { - // If there is a history, use the last interactive entry node - if (histories && histories.length > 0) { - const entryNodeIds = getLastInteractiveValue(histories)?.entryNodeIds; - + if (lastInteractive) { + const entryNodeIds = lastInteractive.entryNodeIds || []; if (Array.isArray(entryNodeIds) && entryNodeIds.length > 0) { return entryNodeIds; } @@ -396,10 +405,10 @@ export const textAdaptGptResponse = ({ /* Update runtimeNode's outputs with interactive data from history */ export function rewriteNodeOutputByHistories( - histories: ChatItemType[], - runtimeNodes: RuntimeNodeItemType[] + runtimeNodes: RuntimeNodeItemType[], + lastInteractive?: InteractiveNodeResponseType ) { - const interactive = getLastInteractiveValue(histories); + const interactive = lastInteractive; if (!interactive?.nodeOutputs) { return runtimeNodes; } diff --git a/packages/global/core/workflow/template/system/interactive/type.d.ts b/packages/global/core/workflow/template/system/interactive/type.d.ts index 9a28cc43f744..f44f1ea887b6 100644 --- a/packages/global/core/workflow/template/system/interactive/type.d.ts +++ b/packages/global/core/workflow/template/system/interactive/type.d.ts @@ -1,6 +1,5 @@ import type { NodeOutputItemType } from '../../../../chat/type'; import type { FlowNodeOutputItemType } from '../../../type/io'; -import type { RuntimeEdgeItemType } from '../../../runtime/type'; import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant'; import { WorkflowIOValueTypeEnum } from 'core/workflow/constants'; import type { ChatCompletionMessageParam } from '../../../../ai/type'; @@ -9,7 +8,6 @@ type InteractiveBasicType = { entryNodeIds: string[]; memoryEdges: RuntimeEdgeItemType[]; nodeOutputs: NodeOutputItemType[]; - toolParams?: { entryNodeIds: string[]; // 记录工具中,交互节点的 Id,而不是起始工作流的入口 memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages @@ -23,6 +21,13 @@ type InteractiveNodeType = { nodeOutputs?: NodeOutputItemType[]; }; +type ChildrenInteractive = InteractiveNodeType & { + type: 'childrenInteractive'; + params: { + childrenResponse?: WorkflowInteractiveResponseType; + }; +}; + export type UserSelectOptionItemType = { key: string; value: string; @@ -62,5 +67,9 @@ type UserInputInteractive = InteractiveNodeType & { submitted?: boolean; }; }; -export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive; + +export type InteractiveNodeResponseType = + | UserSelectInteractive + | UserInputInteractive + | ChildrenInteractive; export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType; diff --git a/packages/global/support/operationLog/constants.ts b/packages/global/support/operationLog/constants.ts new file mode 100644 index 000000000000..9052e79b750b --- /dev/null +++ b/packages/global/support/operationLog/constants.ts @@ -0,0 +1,14 @@ +export enum OperationLogEventEnum { + LOGIN = 'LOGIN', + CREATE_INVITATION_LINK = 'CREATE_INVITATION_LINK', + JOIN_TEAM = 'JOIN_TEAM', + CHANGE_MEMBER_NAME = 'CHANGE_MEMBER_NAME', + KICK_OUT_TEAM = 'KICK_OUT_TEAM', + CREATE_DEPARTMENT = 'CREATE_DEPARTMENT', + CHANGE_DEPARTMENT = 'CHANGE_DEPARTMENT', + DELETE_DEPARTMENT = 'DELETE_DEPARTMENT', + RELOCATE_DEPARTMENT = 'RELOCATE_DEPARTMENT', + CREATE_GROUP = 'CREATE_GROUP', + DELETE_GROUP = 'DELETE_GROUP', + ASSIGN_PERMISSION = 'ASSIGN_PERMISSION' +} diff --git a/packages/global/support/operationLog/type.d.ts b/packages/global/support/operationLog/type.d.ts new file mode 100644 index 000000000000..85cacbd39041 --- /dev/null +++ b/packages/global/support/operationLog/type.d.ts @@ -0,0 +1,19 @@ +import { SourceMemberType } from '../user/type'; +import { OperationLogEventEnum } from './constants'; + +export type OperationLogSchema = { + _id: string; + tmbId: string; + teamId: string; + timestamp: Date; + event: `${OperationLogEventEnum}`; + metadata?: Record; +}; + +export type OperationListItemType = { + _id: string; + sourceMember: SourceMemberType; + event: `${OperationLogEventEnum}`; + timestamp: Date; + metadata: Record; +}; diff --git a/packages/global/support/permission/user/controller.ts b/packages/global/support/permission/user/controller.ts index 87a25f97f5da..9a65333c5bb1 100644 --- a/packages/global/support/permission/user/controller.ts +++ b/packages/global/support/permission/user/controller.ts @@ -1,6 +1,8 @@ import { PerConstructPros, Permission } from '../controller'; import { + TeamApikeyCreatePermissionVal, TeamAppCreatePermissionVal, + TeamDatasetCreatePermissionVal, TeamDefaultPermissionVal, TeamPermissionList } from './constant'; @@ -23,8 +25,8 @@ export class TeamPermission extends Permission { this.setUpdatePermissionCallback(() => { this.hasAppCreatePer = this.checkPer(TeamAppCreatePermissionVal); - this.hasDatasetCreatePer = this.checkPer(TeamAppCreatePermissionVal); - this.hasApikeyCreatePer = this.checkPer(TeamAppCreatePermissionVal); + this.hasDatasetCreatePer = this.checkPer(TeamDatasetCreatePermissionVal); + this.hasApikeyCreatePer = this.checkPer(TeamApikeyCreatePermissionVal); }); } } diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 5a437c9a79dc..685ea22509fc 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -16,6 +16,7 @@ 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'; type Props = { chatId: string; @@ -209,34 +210,24 @@ export const updateInteractiveChat = async ({ } })(); - if (interactiveValue.interactive.type === 'userSelect') { - interactiveValue.interactive = { - ...interactiveValue.interactive, - params: { - ...interactiveValue.interactive.params, - userSelectedVal: userInteractiveVal - } - }; + let finalInteractive = extractDeepestInteractive(interactiveValue.interactive); + + if (finalInteractive.type === 'userSelect') { + finalInteractive.params.userSelectedVal = userInteractiveVal; } else if ( - interactiveValue.interactive.type === 'userInput' && + finalInteractive.type === 'userInput' && typeof parsedUserInteractiveVal === 'object' ) { - interactiveValue.interactive = { - ...interactiveValue.interactive, - params: { - ...interactiveValue.interactive.params, - inputForm: interactiveValue.interactive.params.inputForm.map((item) => { - const itemValue = parsedUserInteractiveVal[item.label]; - return itemValue !== undefined - ? { - ...item, - value: itemValue - } - : item; - }), - submitted: true - } - }; + 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) { diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 9a550b036350..98fb4ccfe6dc 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -141,6 +141,7 @@ export async function dispatchWorkFlow(data: Props): Promise 20) { return { @@ -161,25 +162,28 @@ export async function dispatchWorkFlow(data: Props): Promise { - setTimeout(() => { - props?.workflowStreamResponse?.({ - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: '' - }) - }); - sendStreamTimerSign(); - }, 10000); - }; - sendStreamTimerSign(); + if (isRootRuntime) { + res?.setHeader('Connection', 'keep-alive'); // Set keepalive for long connection + if (stream && res) { + res.setHeader('Content-Type', 'text/event-stream;charset=utf-8'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('X-Accel-Buffering', 'no'); + res.setHeader('Cache-Control', 'no-cache, no-transform'); + + // 10s sends a message to prevent the browser from thinking that the connection is disconnected + const sendStreamTimerSign = () => { + setTimeout(() => { + props?.workflowStreamResponse?.({ + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: '' + }) + }); + sendStreamTimerSign(); + }, 10000); + }; + sendStreamTimerSign(); + } } variables = { @@ -325,10 +329,9 @@ export async function dispatchWorkFlow(data: Props): Promise checkNodeCanRun(node, skippedNodeIdList))) ).flat(); + if (res?.closed) { + addLog.warn('Request is closed', { + appId: props.runningAppInfo.id, + nodeId: node.nodeId, + nodeName: node.name + }); + return []; + } + return [ ...nextStepActiveNodes, ...nextStepSkipNodes, @@ -632,7 +636,7 @@ export async function dispatchWorkFlow(data: Props): Promise; type Response = DispatchNodeResultType<{ + [DispatchNodeResponseKeyEnum.interactive]?: ChildrenInteractive; [NodeOutputKeyEnum.answerText]: string; [NodeOutputKeyEnum.history]: ChatItemType[]; }>; @@ -36,6 +38,7 @@ export const dispatchRunAppNode = async (props: Props): Promise => { runningAppInfo, histories, query, + lastInteractive, node: { pluginId: appId, version }, workflowStreamResponse, params, @@ -100,31 +103,41 @@ export const dispatchRunAppNode = async (props: Props): Promise => { appId: String(appData._id) }; - const { flowResponses, flowUsages, assistantResponses, runTimes } = await dispatchWorkFlow({ - ...props, - // Rewrite stream mode - ...(system_forbid_stream - ? { - stream: false, - workflowStreamResponse: undefined - } - : {}), - runningAppInfo: { - id: String(appData._id), - teamId: String(appData.teamId), - tmbId: String(appData.tmbId), - isChildApp: true - }, - runtimeNodes: storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)), - runtimeEdges: initWorkflowEdgeStatus(edges), - histories: chatHistories, - variables: childrenRunVariables, - query: runtimePrompt2ChatsValue({ - files: userInputFiles, - text: userChatInput - }), - chatConfig - }); + const childrenInteractive = + lastInteractive?.type === 'childrenInteractive' + ? lastInteractive.params.childrenResponse + : undefined; + const entryNodeIds = getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined); + const runtimeNodes = storeNodes2RuntimeNodes(nodes, entryNodeIds); + const runtimeEdges = initWorkflowEdgeStatus(edges, childrenInteractive); + const theQuery = childrenInteractive + ? query + : runtimePrompt2ChatsValue({ files: userInputFiles, text: userChatInput }); + + const { flowResponses, flowUsages, assistantResponses, runTimes, workflowInteractiveResponse } = + await dispatchWorkFlow({ + ...props, + lastInteractive: childrenInteractive, + // Rewrite stream mode + ...(system_forbid_stream + ? { + stream: false, + workflowStreamResponse: undefined + } + : {}), + runningAppInfo: { + id: String(appData._id), + teamId: String(appData.teamId), + tmbId: String(appData.tmbId), + isChildApp: true + }, + runtimeNodes, + runtimeEdges, + histories: chatHistories, + variables: childrenRunVariables, + query: theQuery, + chatConfig + }); const completeMessages = chatHistories.concat([ { @@ -142,6 +155,14 @@ export const dispatchRunAppNode = async (props: Props): Promise => { const usagePoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0); return { + [DispatchNodeResponseKeyEnum.interactive]: workflowInteractiveResponse + ? { + type: 'childrenInteractive', + params: { + childrenResponse: workflowInteractiveResponse + } + } + : undefined, assistantResponses: system_forbid_stream ? [] : assistantResponses, [DispatchNodeResponseKeyEnum.runTimes]: runTimes, [DispatchNodeResponseKeyEnum.nodeResponse]: { diff --git a/packages/service/support/operationLog/addOperationLog.ts b/packages/service/support/operationLog/addOperationLog.ts new file mode 100644 index 000000000000..af692b7ca5e2 --- /dev/null +++ b/packages/service/support/operationLog/addOperationLog.ts @@ -0,0 +1,26 @@ +import { MongoOperationLog } from './schema'; +import { OperationLogEventEnum } from '@fastgpt/global/support/operationLog/constants'; +import { TemplateParamsMap } from './constants'; +import { retryFn } from '../../../global/common/system/utils'; + +export function addOperationLog({ + teamId, + tmbId, + event, + params +}: { + tmbId: string; + teamId: string; + event: T; + params?: TemplateParamsMap[T]; +}) { + console.log('Insert log'); + retryFn(() => + MongoOperationLog.create({ + tmbId: tmbId, + teamId: teamId, + event, + metadata: params + }) + ); +} diff --git a/packages/service/support/operationLog/constants.ts b/packages/service/support/operationLog/constants.ts new file mode 100644 index 000000000000..4f20a5b3ad00 --- /dev/null +++ b/packages/service/support/operationLog/constants.ts @@ -0,0 +1,85 @@ +import { OperationLogEventEnum } from '@fastgpt/global/support/operationLog/constants'; +import { i18nT } from '../../../web/i18n/utils'; + +export const operationLogI18nMap = { + [OperationLogEventEnum.LOGIN]: { + content: i18nT('account_team:log_login'), + typeLabel: i18nT('account_team:login') + }, + [OperationLogEventEnum.CREATE_INVITATION_LINK]: { + content: i18nT('account_team:log_create_invitation_link'), + typeLabel: i18nT('account_team:create_invitation_link') + }, + [OperationLogEventEnum.JOIN_TEAM]: { + content: i18nT('account_team:log_join_team'), + typeLabel: i18nT('account_team:join_team') + }, + [OperationLogEventEnum.CHANGE_MEMBER_NAME]: { + content: i18nT('account_team:log_change_member_name'), + typeLabel: i18nT('account_team:change_member_name') + }, + [OperationLogEventEnum.KICK_OUT_TEAM]: { + content: i18nT('account_team:log_kick_out_team'), + typeLabel: i18nT('account_team:kick_out_team') + }, + [OperationLogEventEnum.CREATE_DEPARTMENT]: { + content: i18nT('account_team:log_create_department'), + typeLabel: i18nT('account_team:create_department') + }, + [OperationLogEventEnum.CHANGE_DEPARTMENT]: { + content: i18nT('account_team:log_change_department'), + typeLabel: i18nT('account_team:change_department_name') + }, + [OperationLogEventEnum.DELETE_DEPARTMENT]: { + content: i18nT('account_team:log_delete_department'), + typeLabel: i18nT('account_team:delete_department') + }, + [OperationLogEventEnum.RELOCATE_DEPARTMENT]: { + content: i18nT('account_team:log_relocate_department'), + typeLabel: i18nT('account_team:relocate_department') + }, + [OperationLogEventEnum.CREATE_GROUP]: { + content: i18nT('account_team:log_create_group'), + typeLabel: i18nT('account_team:create_group') + }, + [OperationLogEventEnum.DELETE_GROUP]: { + content: i18nT('account_team:log_delete_group'), + typeLabel: i18nT('account_team:delete_group') + }, + [OperationLogEventEnum.ASSIGN_PERMISSION]: { + content: i18nT('account_team:log_assign_permission'), + typeLabel: i18nT('account_team:assign_permission') + } +} as const; + +export type TemplateParamsMap = { + [OperationLogEventEnum.LOGIN]: { name?: string }; + [OperationLogEventEnum.CREATE_INVITATION_LINK]: { name?: string; link: string }; + [OperationLogEventEnum.JOIN_TEAM]: { name?: string; link: string }; + [OperationLogEventEnum.CHANGE_MEMBER_NAME]: { + name?: string; + memberName: string; + newName: string; + }; + [OperationLogEventEnum.KICK_OUT_TEAM]: { + name?: string; + memberName: string; + }; + [OperationLogEventEnum.CREATE_DEPARTMENT]: { name?: string; departmentName: string }; + [OperationLogEventEnum.CHANGE_DEPARTMENT]: { + name?: string; + departmentName: string; + }; + [OperationLogEventEnum.DELETE_DEPARTMENT]: { name?: string; departmentName: string }; + [OperationLogEventEnum.RELOCATE_DEPARTMENT]: { + name?: string; + departmentName: string; + }; + [OperationLogEventEnum.CREATE_GROUP]: { name?: string; groupName: string }; + [OperationLogEventEnum.DELETE_GROUP]: { name?: string; groupName: string }; + [OperationLogEventEnum.ASSIGN_PERMISSION]: { + name?: string; + objectName: string; + permission: string; + }; +}; diff --git a/packages/service/support/operationLog/schema.ts b/packages/service/support/operationLog/schema.ts new file mode 100644 index 000000000000..6e2a84e1bc5c --- /dev/null +++ b/packages/service/support/operationLog/schema.ts @@ -0,0 +1,40 @@ +import { Schema, getMongoLogModel } from '../../common/mongo'; +import type { OperationLogSchema } from '@fastgpt/global/support/operationLog/type'; +import { OperationLogEventEnum } from '@fastgpt/global/support/operationLog/constants'; +import { + TeamCollectionName, + TeamMemberCollectionName +} from '@fastgpt/global/support/user/team/constant'; + +export const OperationLogCollectionName = 'operationLog'; + +const OperationLogSchema = new Schema({ + tmbId: { + type: Schema.Types.ObjectId, + ref: TeamMemberCollectionName, + required: true + }, + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + timestamp: { + type: Date, + default: () => new Date() + }, + event: { + type: String, + enum: Object.values(OperationLogEventEnum), + required: true + }, + metadata: { + type: Object, + default: {} + } +}); + +export const MongoOperationLog = getMongoLogModel( + OperationLogCollectionName, + OperationLogSchema +); diff --git a/packages/service/support/user/utils.ts b/packages/service/support/user/utils.ts index 60e633fbe265..b01cc46d4f6d 100644 --- a/packages/service/support/user/utils.ts +++ b/packages/service/support/user/utils.ts @@ -104,8 +104,11 @@ export async function addSourceMember({ const tmb = tmbList.find((tmb) => String(tmb._id) === String(item.tmbId)); if (!tmb) return; + // @ts-ignore + const formatItem = typeof item.toObject === 'function' ? item.toObject() : item; + return { - ...item, + ...formatItem, sourceMember: { name: tmb.name, avatar: tmb.avatar, status: tmb.status } }; }) diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 135e72a98d78..b7e1a4744620 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -5,17 +5,23 @@ "7days": "7 Days", "accept": "accept", "action": "operate", + "assign_permission": "Permission change", + "change_department_name": "Department Editor", + "change_member_name": "Member name change", "confirm_delete_group": "Confirm to delete group?", "confirm_delete_member": "Confirm to delete member?", "confirm_delete_org": "Confirm to delete organization?", "confirm_forbidden": "Confirm forbidden", "confirm_leave_team": "Confirmed to leave the team? \nAfter exiting, all your resources in the team are transferred to the team owner.", "copy_link": "Copy link", + "create_department": "Create a sub-department", "create_group": "Create group", "create_invitation_link": "Create Invitation Link", "create_org": "Create organization", "create_sub_org": "Create sub-organization", "delete": "delete", + "delete_department": "Delete sub-department", + "delete_group": "Delete a group", "delete_org": "Delete organization", "edit_info": "Edit information", "edit_member": "Edit user", @@ -37,21 +43,51 @@ "invitation_link_list": "Invitation link list", "invite_member": "Invite members", "invited": "Invited", + "join_team": "Join the team", + "kick_out_team": "Remove members", "label_sync": "Tag sync", "leave_team_failed": "Leaving the team exception", + "log_assign_permission": "[{{name}}] Updated the permissions of [{{objectName}}]: [Application creation: [{{appCreate}}], Knowledge Base: [{{datasetCreate}}], API Key: [{{apiKeyCreate}}], Management: [{{manage}}]]", + "log_change_department": "【{{name}}】Updated department【{{departmentName}}】", + "log_change_member_name": "【{{name}}】Rename member [{{memberName}}] to 【{{newName}}】", + "log_create_department": "【{{name}}】Department【{{departmentName}}】", + "log_create_group": "【{{name}}】Created group [{{groupName}}]", + "log_create_invitation_link": "【{{name}}】Created invitation link【{{link}}】", + "log_delete_department": "{{name}} deleted department {{departmentName}}", + "log_delete_group": "{{name}} deleted group {{groupName}}", + "log_details": "Details", + "log_join_team": "【{{name}}】Join the team through the invitation link 【{{link}}】", + "log_kick_out_team": "{{name}} removed member {{memberName}}", + "log_login": "【{{name}}】Logined in the system", + "log_relocate_department": "【{{name}}】Displayed department【{{departmentName}}】", + "log_time": "Operation time", + "log_type": "Operation Type", + "log_user": "Operator", + "login": "Log in", "manage_member": "Managing members", "member": "member", "member_group": "Belonging to member group", "move_member": "Move member", "move_org": "Move organization", + "operation_log": "log", "org": "organization", "org_description": "Organization description", "org_name": "Organization name", "owner": "owner", "permission": "Permissions", + "permission_apikeyCreate": "Create API Key", + "permission_apikeyCreate_Tip": "Can create global APIKeys", + "permission_appCreate": "Create Application", + "permission_appCreate_tip": "Can create applications in the root directory (creation permissions in folders are controlled by the folder)", + "permission_datasetCreate": "Create Knowledge Base", + "permission_datasetCreate_Tip": "Can create knowledge bases in the root directory (creation permissions in folders are controlled by the folder)", + "permission_manage": "Admin", + "permission_manage_tip": "Can manage members, create groups, manage all groups, and assign permissions to groups and members", + "relocate_department": "Department Mobile", "remark": "remark", "remove_tip": "Confirm to remove {{username}} from the team?", "retain_admin_permissions": "Keep administrator rights", + "search_log": "Search log", "search_member_group_name": "Search member/group name", "total_team_members": "{{amount}} members in total", "transfer_ownership": "transfer owner", @@ -61,13 +97,5 @@ "user_team_invite_member": "Invite members", "user_team_leave_team": "Leave the team", "user_team_leave_team_failed": "Failure to leave the team", - "waiting": "To be accepted", - "permission_appCreate": "Create Application", - "permission_datasetCreate": "Create Knowledge Base", - "permission_apikeyCreate": "Create API Key", - "permission_appCreate_tip": "Can create applications in the root directory (creation permissions in folders are controlled by the folder)", - "permission_datasetCreate_Tip": "Can create knowledge bases in the root directory (creation permissions in folders are controlled by the folder)", - "permission_apikeyCreate_Tip": "Can create global APIKeys", - "permission_manage": "Admin", - "permission_manage_tip": "Can manage members, create groups, manage all groups, and assign permissions to groups and members" + "waiting": "To be accepted" } diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index c612cbd4ff59..a98318bd4f2e 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -5,6 +5,9 @@ "7days": "7天", "accept": "接受", "action": "操作", + "assign_permission": "权限变更", + "change_department_name": "部门编辑", + "change_member_name": "成员改名", "confirm_delete_from_org": "确认将 {{username}} 移出部门?", "confirm_delete_from_team": "确认将 {{username}} 移出团队?", "confirm_delete_group": "确认删除群组?", @@ -12,13 +15,16 @@ "confirm_forbidden": "确认停用", "confirm_leave_team": "确认离开该团队? \n退出后,您在该团队所有的资源均转让给团队所有者。", "copy_link": "复制链接", + "create_department": "创建子部门", "create_group": "创建群组", "create_invitation_link": "创建邀请链接", "create_org": "创建部门", "create_sub_org": "创建子部门", "delete": "删除", + "delete_department": "删除子部门", "delete_from_org": "移出部门", "delete_from_team": "移出团队", + "delete_group": "删除群组", "delete_org": "删除部门", "edit_info": "编辑信息", "edit_member": "编辑用户", @@ -41,27 +47,37 @@ "invitation_link_list": "链接列表", "invite_member": "邀请成员", "invited": "已邀请", + "join_team": "加入团队", "join_update_time": "加入/更新时间", + "kick_out_team": "移除成员", "label_sync": "标签同步", "leave": "已离职", "leave_team_failed": "离开团队异常", + "log_details": "详情", + "log_time": "操作时间", + "log_type": "操作类型", + "log_user": "操作人员", + "login": "登录", "manage_member": "管理成员", "member": "成员", "member_group": "所属群组", "move_member": "移动成员", "move_org": "移动部门", "notification_recieve": "团队通知接收", + "operation_log": "日志", "org": "部门", "org_description": "介绍", "org_name": "部门名称", "owner": "所有者", "permission": "权限", "please_bind_contact": "请绑定联系方式", + "relocate_department": "部门移动", "remark": "备注", "remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“已离职”,不删除操作数据,账号下资源自动转让给团队所有者。", "restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。", "restore_tip_title": "恢复确认", "retain_admin_permissions": "保留管理员权限", + "search_log": "搜索日志", "search_member": "搜索成员", "search_member_group_name": "搜索成员/群组名称", "search_org": "搜索部门", @@ -85,5 +101,17 @@ "permission_datasetCreate_Tip": "可以在根目录创建知识库,(文件夹下的创建权限由文件夹控制)", "permission_apikeyCreate_Tip": "可以创建全局的 APIKey", "permission_manage": "管理员", - "permission_manage_tip": "可以管理成员、创建群组、管理所有群组、为群组和成员分配权限" + "permission_manage_tip": "可以管理成员、创建群组、管理所有群组、为群组和成员分配权限", + "log_login": "【{{name}}】登录了系统", + "log_create_invitation_link": "【{{name}}】创建了邀请链接【{{link}}】", + "log_join_team": "【{{name}}】通过邀请链接【{{link}}】加入团队", + "log_change_member_name": "【{{name}}】将成员【{{memberName}}】重命名为【{{newName}}】", + "log_kick_out_team": "【{{name}}】移除了成员【{{memberName}}】", + "log_create_department": "【{{name}}】创建了部门【{{departmentName}}】", + "log_change_department": "【{{name}}】更新了部门【{{departmentName}}】", + "log_delete_department": "【{{name}}】删除了部门【{{departmentName}}】", + "log_relocate_department": "【{{name}}】移动了部门【{{departmentName}}】", + "log_create_group": "【{{name}}】创建了群组【{{groupName}}】", + "log_delete_group": "【{{name}}】删除了群组【{{groupName}}】", + "log_assign_permission": "【{{name}}】更新了【{{objectName}}】的权限:[应用创建:【{{appCreate}}】, 知识库:【{{datasetCreate}}】, API密钥:【{{apiKeyCreate}}】, 管理:【{{manage}}】]" } diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index eac43e6f0d09..8b55a9276d5f 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -5,17 +5,23 @@ "7days": "7 天", "accept": "接受", "action": "操作", + "assign_permission": "權限變更", + "change_department_name": "部門編輯", + "change_member_name": "成員改名", "confirm_delete_group": "確認刪除群組?", "confirm_delete_member": "確認刪除成員?", "confirm_delete_org": "確認刪除該部門?", "confirm_forbidden": "確認停用", "confirm_leave_team": "確認離開該團隊? \n結束後,您在該團隊所有的資源轉讓給團隊所有者。", "copy_link": "複製連結", + "create_department": "創建子部門", "create_group": "建立群組", "create_invitation_link": "建立邀請連結", "create_org": "建立部門", "create_sub_org": "建立子部門", "delete": "刪除", + "delete_department": "刪除子部門", + "delete_group": "刪除群組", "delete_org": "刪除部門", "edit_info": "編輯訊息", "edit_member": "編輯使用者", @@ -37,21 +43,51 @@ "invitation_link_list": "連結列表", "invite_member": "邀請成員", "invited": "已邀請", + "join_team": "加入團隊", + "kick_out_team": "移除成員", "label_sync": "標籤同步", "leave_team_failed": "離開團隊異常", + "log_assign_permission": "【{{name}}】更新了【{{objectName}}】的權限:[應用創建:【{{appCreate}}】, 知識庫:【{{datasetCreate}}】, API密鑰:【{{apiKeyCreate}}】, 管理:【{{manage}}】]", + "log_change_department": "【{{name}}】更新了部門【{{departmentName}}】", + "log_change_member_name": "【{{name}}】將成員【{{memberName}}】重命名為【{{newName}}】", + "log_create_department": "【{{name}}】創建了部門【{{departmentName}}】", + "log_create_group": "【{{name}}】創建了群組【{{groupName}}】", + "log_create_invitation_link": "【{{name}}】創建了邀請鏈接【{{link}}】", + "log_delete_department": "{{name}} 刪除了部門 {{departmentName}}", + "log_delete_group": "{{name}} 刪除了群組 {{groupName}}", + "log_details": "詳情", + "log_join_team": "【{{name}}】通過邀請鏈接【{{link}}】加入團隊", + "log_kick_out_team": "{{name}} 移除了成員 {{memberName}}", + "log_login": "【{{name}}】登錄了系統", + "log_relocate_department": "【{{name}}】移動了部門【{{departmentName}}】", + "log_time": "操作時間", + "log_type": "操作類型", + "log_user": "操作人員", + "login": "登入", "manage_member": "管理成員", "member": "成員", "member_group": "所屬成員組", "move_member": "移動成員", "move_org": "行動部門", + "operation_log": "紀錄", "org": "組織", "org_description": "介紹", "org_name": "部門名稱", "owner": "擁有者", "permission": "權限", + "permission_apikeyCreate": "建立 API 密鑰", + "permission_apikeyCreate_Tip": "可以建立全域的 APIKey", + "permission_appCreate": "建立應用程式", + "permission_appCreate_tip": "可以在根目錄建立應用程式,(資料夾下的建立權限由資料夾控制)", + "permission_datasetCreate": "建立知識庫", + "permission_datasetCreate_Tip": "可以在根目錄建立知識庫,(資料夾下的建立權限由資料夾控制)", + "permission_manage": "管理員", + "permission_manage_tip": "可以管理成員、建立群組、管理所有群組、為群組和成員分配權限", + "relocate_department": "部門移動", "remark": "備註", "remove_tip": "確認將 {{username}} 移出團隊?", "retain_admin_permissions": "保留管理員權限", + "search_log": "搜索日誌", "search_member_group_name": "搜尋成員/群組名稱", "total_team_members": "共 {{amount}} 名成員", "transfer_ownership": "轉讓所有者", @@ -61,13 +97,5 @@ "user_team_invite_member": "邀請成員", "user_team_leave_team": "離開團隊", "user_team_leave_team_failed": "離開團隊失敗", - "waiting": "待接受", - "permission_appCreate": "建立應用程式", - "permission_datasetCreate": "建立知識庫", - "permission_apikeyCreate": "建立 API 密鑰", - "permission_appCreate_tip": "可以在根目錄建立應用程式,(資料夾下的建立權限由資料夾控制)", - "permission_datasetCreate_Tip": "可以在根目錄建立知識庫,(資料夾下的建立權限由資料夾控制)", - "permission_apikeyCreate_Tip": "可以建立全域的 APIKey", - "permission_manage": "管理員", - "permission_manage_tip": "可以管理成員、建立群組、管理所有群組、為群組和成員分配權限" + "waiting": "待接受" } diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts index b090f38d6431..5eaa3b42ed15 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts @@ -6,6 +6,7 @@ import { import { ChatBoxInputType, UserInputFileItemType } from './type'; import { getFileIcon } from '@fastgpt/global/common/file/icon'; import { ChatItemValueTypeEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants'; +import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => { if (!value) { @@ -82,17 +83,19 @@ export const setUserSelectResultToHistories = ( i !== item.value.length - 1 || val.type !== ChatItemValueTypeEnum.interactive || !val.interactive - ) + ) { return val; + } - if (val.interactive.type === 'userSelect') { + const finalInteractive = extractDeepestInteractive(val.interactive); + if (finalInteractive.type === 'userSelect') { return { ...val, interactive: { - ...val.interactive, + ...finalInteractive, params: { - ...val.interactive.params, - userSelectedVal: val.interactive.params.userSelectOptions.find( + ...finalInteractive.params, + userSelectedVal: finalInteractive.params.userSelectOptions.find( (item) => item.value === interactiveVal )?.value } @@ -100,13 +103,13 @@ export const setUserSelectResultToHistories = ( }; } - if (val.interactive.type === 'userInput') { + if (finalInteractive.type === 'userInput') { return { ...val, interactive: { - ...val.interactive, + ...finalInteractive, params: { - ...val.interactive.params, + ...finalInteractive.params, submitted: true } } diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index cb3a26a8fa92..09cfff404bd0 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -28,6 +28,7 @@ import { isEqual } from 'lodash'; import { useTranslation } from 'next-i18next'; import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus'; import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents'; +import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; const accordionButtonStyle = { w: 'auto', @@ -245,11 +246,12 @@ const AIResponseBox = ({ return ; } if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) { - if (value.interactive.type === 'userSelect') { - return ; + const finalInteractive = extractDeepestInteractive(value.interactive); + if (finalInteractive.type === 'userSelect') { + return ; } - if (value.interactive?.type === 'userInput') { - return ; + if (finalInteractive.type === 'userInput') { + return ; } } return null; diff --git a/projects/app/src/pageComponents/account/team/MemberTable.tsx b/projects/app/src/pageComponents/account/team/MemberTable.tsx index 425fc6f7ab0d..d60ec4cc601b 100644 --- a/projects/app/src/pageComponents/account/team/MemberTable.tsx +++ b/projects/app/src/pageComponents/account/team/MemberTable.tsx @@ -303,7 +303,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { })()} - + {format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')} {member.updateTime diff --git a/projects/app/src/pageComponents/account/team/OperationLog/index.tsx b/projects/app/src/pageComponents/account/team/OperationLog/index.tsx new file mode 100644 index 000000000000..bf85a21ffc00 --- /dev/null +++ b/projects/app/src/pageComponents/account/team/OperationLog/index.tsx @@ -0,0 +1,102 @@ +import { + Box, + Button, + Flex, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; +import { getOperationLogs } from '@/web/support/user/team/operantionLog/api'; +import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; +import { operationLogI18nMap } from '@fastgpt/service/support/operationLog/constants'; +import { OperationLogEventEnum } from '@fastgpt/global/support/operationLog/constants'; +import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; +import UserBox from '@fastgpt/web/components/common/UserBox'; + +function OperationLogTable({ Tabs }: { Tabs: React.ReactNode }) { + const { t } = useTranslation(); + + const [searchKey, setSearchKey] = useState(''); + const { + data: operationLogs = [], + isLoading: loadingLogs, + ScrollData: LogScrollData + } = useScrollPagination(getOperationLogs, { + pageSize: 20, + refreshDeps: [searchKey], + throttleWait: 500, + debounceWait: 200 + }); + + const isLoading = loadingLogs; + + return ( + <> + + {Tabs} + + + + + + + + + + + + + + + + {operationLogs?.map((log) => { + const i18nData = operationLogI18nMap[log.event]; + const metadata = { ...log.metadata }; + + if (log.event === OperationLogEventEnum.ASSIGN_PERMISSION) { + const permissionValue = parseInt(metadata.permission, 10); + + const permission = new TeamPermission({ per: permissionValue }); + metadata.appCreate = permission.hasAppCreatePer ? '✔' : '✘'; + metadata.datasetCreate = permission.hasDatasetCreatePer ? '✔' : '✘'; + metadata.apiKeyCreate = permission.hasApikeyCreatePer ? '✔' : '✘'; + metadata.manage = permission.hasManagePer ? '✔' : '✘'; + } + + return i18nData ? ( + + + + + + + ) : null; + })} + +
+ {t('account_team:log_user')} + {t('account_team:log_time')}{t('account_team:log_type')}{t('account_team:log_details')}
+ + {formatTime2YMDHMS(log.timestamp)}{t(i18nData.typeLabel)}{t(i18nData.content, metadata as any) as string}
+
+
+
+ + ); +} + +export default OperationLogTable; diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderDebug/NodeDebugResponse.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderDebug/NodeDebugResponse.tsx index c1065234d8cf..e260eef42ede 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderDebug/NodeDebugResponse.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/RenderDebug/NodeDebugResponse.tsx @@ -13,7 +13,10 @@ import { SelectOptionsComponent } from '@/components/core/chat/components/Interactive/InteractiveComponents'; import { UserInputInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; -import { initWorkflowEdgeStatus } from '@fastgpt/global/core/workflow/runtime/utils'; +import { + getLastInteractiveValue, + initWorkflowEdgeStatus +} from '@fastgpt/global/core/workflow/runtime/utils'; import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; @@ -130,10 +133,11 @@ const NodeDebugResponse = ({ nodeId, debugResult }: NodeDebugResponseProps) => { } ]; + const lastInteractive = getLastInteractiveValue(mockHistory); onNextNodeDebug({ ...workflowDebugData, // Rewrite runtimeEdges - runtimeEdges: initWorkflowEdgeStatus(workflowDebugData.runtimeEdges, mockHistory), + runtimeEdges: initWorkflowEdgeStatus(workflowDebugData.runtimeEdges, lastInteractive), query: updatedQuery, history: mockHistory }); diff --git a/projects/app/src/pages/account/team/index.tsx b/projects/app/src/pages/account/team/index.tsx index 5d613dbb5813..1405016e9d50 100644 --- a/projects/app/src/pages/account/team/index.tsx +++ b/projects/app/src/pages/account/team/index.tsx @@ -18,6 +18,7 @@ const MemberTable = dynamic(() => import('@/pageComponents/account/team/MemberTa const PermissionManage = dynamic( () => import('@/pageComponents/account/team/PermissionManage/index') ); +const OperationLogTable = dynamic(() => import('@/pageComponents/account/team/OperationLog/index')); const GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index')); const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index')); const HandleInviteModal = dynamic( @@ -28,7 +29,8 @@ export enum TeamTabEnum { member = 'member', org = 'org', group = 'group', - permission = 'permission' + permission = 'permission', + operationLog = 'operationLog' } const Team = () => { @@ -57,7 +59,8 @@ const Team = () => { { label: t('account_team:member'), value: TeamTabEnum.member }, { label: t('account_team:org'), value: TeamTabEnum.org }, { label: t('account_team:group'), value: TeamTabEnum.group }, - { label: t('account_team:permission'), value: TeamTabEnum.permission } + { label: t('account_team:permission'), value: TeamTabEnum.permission }, + { label: t('account_team:operation_log'), value: TeamTabEnum.operationLog } ]} px={'1rem'} value={teamTab} @@ -150,6 +153,7 @@ const Team = () => { {teamTab === TeamTabEnum.org && } {teamTab === TeamTabEnum.group && } {teamTab === TeamTabEnum.permission && } + {teamTab === TeamTabEnum.operationLog && }
{invitelinkid && } diff --git a/projects/app/src/pages/api/core/chat/chatTest.ts b/projects/app/src/pages/api/core/chat/chatTest.ts index 402ea353ef8d..2570aca57d0e 100644 --- a/projects/app/src/pages/api/core/chat/chatTest.ts +++ b/projects/app/src/pages/api/core/chat/chatTest.ts @@ -98,7 +98,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const isPlugin = app.type === AppTypeEnum.plugin; - const userQuestion: UserChatItemType = (() => { + const userQuestion: UserChatItemType = await (async () => { if (isPlugin) { return getPluginRunUserQuery({ pluginInputs: getPluginInputsFromStoreNodes(app.modules), @@ -107,9 +107,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }); } - const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined; + const latestHumanChat = chatMessages.pop() as UserChatItemType; if (!latestHumanChat) { - throw new Error('User question is empty'); + return Promise.reject('User question is empty'); } return latestHumanChat; })(); @@ -136,14 +136,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } const newHistories = concatHistories(histories, chatMessages); - + const interactive = getLastInteractiveValue(newHistories) || undefined; // Get runtimeNodes - let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); + let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, interactive)); if (isPlugin) { runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); variables = {}; } - runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes); + runtimeNodes = rewriteNodeOutputByHistories(runtimeNodes, interactive); const workflowResponseWrite = getWorkflowResponseWrite({ res, @@ -175,9 +175,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { chatId, responseChatItemId, runtimeNodes, - runtimeEdges: initWorkflowEdgeStatus(edges, newHistories), + runtimeEdges: initWorkflowEdgeStatus(edges, interactive), variables, query: removeEmptyUserInput(userQuestion.value), + lastInteractive: interactive, chatConfig, histories: newHistories, stream: true, diff --git a/projects/app/src/pages/api/core/workflow/debug.ts b/projects/app/src/pages/api/core/workflow/debug.ts index d7fad05465e3..e4fee5f93088 100644 --- a/projects/app/src/pages/api/core/workflow/debug.ts +++ b/projects/app/src/pages/api/core/workflow/debug.ts @@ -10,6 +10,7 @@ import { NextAPI } from '@/service/middleware/entry'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { defaultApp } from '@/web/core/app/constants'; import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; +import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils'; async function handler( req: NextApiRequest, @@ -44,6 +45,7 @@ async function handler( // auth balance const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(tmbId); + const lastInteractive = getLastInteractiveValue(history); /* start process */ const { flowUsages, flowResponses, debugResponse, newVariables, workflowInteractiveResponse } = @@ -65,6 +67,7 @@ async function handler( }, runtimeNodes: nodes, runtimeEdges: edges, + lastInteractive, variables, query: query, chatConfig: defaultApp.chatConfig, diff --git a/projects/app/src/pages/api/support/user/account/loginByPassword.ts b/projects/app/src/pages/api/support/user/account/loginByPassword.ts index 8c37ce0d2d74..14652df36853 100644 --- a/projects/app/src/pages/api/support/user/account/loginByPassword.ts +++ b/projects/app/src/pages/api/support/user/account/loginByPassword.ts @@ -9,6 +9,8 @@ import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequency import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { UserErrEnum } from '@fastgpt/global/common/error/code/user'; +import { addOperationLog } from '@fastgpt/service/support/operationLog/addOperationLog'; +import { OperationLogEventEnum } from '@fastgpt/global/support/operationLog/constants'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { username, password } = req.body as PostLoginProps; @@ -64,6 +66,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { setCookie(res, token); + addOperationLog({ + tmbId: userDetail.team.tmbId, + teamId: userDetail.team.teamId, + event: OperationLogEventEnum.LOGIN + }); + return { user: userDetail, token diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 5657a2783da5..f0a352d9fa9b 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -139,7 +139,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { // Computed start hook params const startHookText = (() => { // Chat - const userQuestion = chatMessages[chatMessages.length - 1] as UserChatItemType | undefined; + const userQuestion = chatMessages[chatMessages.length - 1] as UserChatItemType; if (userQuestion) return chatValue2RuntimePrompt(userQuestion.value).text; // plugin @@ -245,16 +245,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { // Get chat histories const newHistories = concatHistories(histories, chatMessages); + const interactive = getLastInteractiveValue(newHistories) || undefined; // Get runtimeNodes - let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); + let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, interactive)); if (isPlugin) { // Assign values to runtimeNodes using variables runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); // Plugin runtime does not need global variables(It has been injected into the pluginInputNode) variables = {}; } - runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes); + runtimeNodes = rewriteNodeOutputByHistories(runtimeNodes, interactive); const workflowResponseWrite = getWorkflowResponseWrite({ res, @@ -288,7 +289,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { chatId, responseChatItemId, runtimeNodes, - runtimeEdges: initWorkflowEdgeStatus(edges, newHistories), + runtimeEdges: initWorkflowEdgeStatus(edges, interactive), variables, query: removeEmptyUserInput(userQuestion.value), chatConfig, diff --git a/projects/app/src/pages/api/v2/chat/completions.ts b/projects/app/src/pages/api/v2/chat/completions.ts index 4750ee07cdea..75e336881428 100644 --- a/projects/app/src/pages/api/v2/chat/completions.ts +++ b/projects/app/src/pages/api/v2/chat/completions.ts @@ -139,7 +139,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { // Computed start hook params const startHookText = (() => { // Chat - const userQuestion = chatMessages[chatMessages.length - 1] as UserChatItemType | undefined; + const userQuestion = chatMessages[chatMessages.length - 1] as UserChatItemType; if (userQuestion) return chatValue2RuntimePrompt(userQuestion.value).text; // plugin @@ -245,16 +245,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { // Get chat histories const newHistories = concatHistories(histories, chatMessages); - + const interactive = getLastInteractiveValue(newHistories) || undefined; // Get runtimeNodes - let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); + let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, interactive)); if (isPlugin) { // Assign values to runtimeNodes using variables runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); // Plugin runtime does not need global variables(It has been injected into the pluginInputNode) variables = {}; } - runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes); + runtimeNodes = rewriteNodeOutputByHistories(runtimeNodes, interactive); const workflowResponseWrite = getWorkflowResponseWrite({ res, @@ -288,9 +288,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { chatId, responseChatItemId, runtimeNodes, - runtimeEdges: initWorkflowEdgeStatus(edges, newHistories), + runtimeEdges: initWorkflowEdgeStatus(edges, interactive), variables, query: removeEmptyUserInput(userQuestion.value), + lastInteractive: interactive, chatConfig, histories: newHistories, stream, diff --git a/projects/app/src/service/events/generateQA.ts b/projects/app/src/service/events/generateQA.ts index ebf8a829cd4c..69a5b408914e 100644 --- a/projects/app/src/service/events/generateQA.ts +++ b/projects/app/src/service/events/generateQA.ts @@ -33,9 +33,21 @@ const reduceQueue = () => { return global.qaQueueLen === 0; }; +const reduceQueueAndReturn = (delay = 0) => { + reduceQueue(); + if (delay) { + setTimeout(() => { + generateQA(); + }, delay); + } else { + generateQA(); + } +}; export async function generateQA(): Promise { const max = global.systemEnv?.qaMaxProcess || 10; + addLog.debug(`[QA Queue] Queue size: ${global.qaQueueLen}`); + if (global.qaQueueLen >= max) return; global.qaQueueLen++; @@ -98,14 +110,12 @@ export async function generateQA(): Promise { return; } if (error) { - reduceQueue(); - return generateQA(); + return reduceQueueAndReturn(); } // auth balance if (!(await checkTeamAiPointsAndLock(data.teamId))) { - reduceQueue(); - return generateQA(); + return reduceQueueAndReturn(); } addLog.info(`[QA Queue] Start`); @@ -137,14 +147,8 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`; const qaArr = formatSplitText({ answer, rawText: text, llmModel: modelData }); // 格式化后的QA对 - addLog.info(`[QA Queue] Finish`, { - time: Date.now() - startTime, - splitLength: qaArr.length, - usage: chatResponse.usage - }); - // get vector and insert - const { insertLen } = await pushDataListToTrainingQueueByCollectionId({ + await pushDataListToTrainingQueueByCollectionId({ teamId: data.teamId, tmbId: data.tmbId, collectionId: data.collectionId, @@ -160,21 +164,21 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`; await MongoDatasetTraining.findByIdAndDelete(data._id); // add bill - if (insertLen > 0) { - pushQAUsage({ - teamId: data.teamId, - tmbId: data.tmbId, - inputTokens: await countGptMessagesTokens(messages), - outputTokens: await countPromptTokens(answer), - billId: data.billId, - model: modelData.model - }); - } else { - addLog.info(`QA result 0:`, { answer }); - } + pushQAUsage({ + teamId: data.teamId, + tmbId: data.tmbId, + inputTokens: await countGptMessagesTokens(messages), + outputTokens: await countPromptTokens(answer), + billId: data.billId, + model: modelData.model + }); + addLog.info(`[QA Queue] Finish`, { + time: Date.now() - startTime, + splitLength: qaArr.length, + usage: chatResponse.usage + }); - reduceQueue(); - generateQA(); + return reduceQueueAndReturn(); } catch (err: any) { addLog.error(`[QA Queue] Error`, err); await MongoDatasetTraining.updateOne( @@ -188,9 +192,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`; } ); - setTimeout(() => { - generateQA(); - }, 1000); + return reduceQueueAndReturn(1000); } } diff --git a/projects/app/src/service/events/generateVector.ts b/projects/app/src/service/events/generateVector.ts index 658e5618efd1..c488cbc62cca 100644 --- a/projects/app/src/service/events/generateVector.ts +++ b/projects/app/src/service/events/generateVector.ts @@ -35,6 +35,8 @@ const reduceQueueAndReturn = (delay = 0) => { /* 索引生成队列。每导入一次,就是一个单独的线程 */ export async function generateVector(): Promise { const max = global.systemEnv?.vectorMaxProcess || 10; + addLog.debug(`[Vector Queue] Queue size: ${global.vectorQueueLen}`); + if (global.vectorQueueLen >= max) return; global.vectorQueueLen++; const start = Date.now(); diff --git a/projects/app/src/web/support/user/team/operantionLog/api.ts b/projects/app/src/web/support/user/team/operantionLog/api.ts new file mode 100644 index 000000000000..728d3b0df4eb --- /dev/null +++ b/projects/app/src/web/support/user/team/operantionLog/api.ts @@ -0,0 +1,9 @@ +import { GET, POST, PUT } from '@/web/common/api/request'; +import type { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import type { OperationListItemType } from '@fastgpt/global/support/operationLog/type'; + +export const getOperationLogs = (props: PaginationProps) => + POST>( + `/proApi/support/user/team/operationLog/list`, + props + );