V4.9.5 feature (#4520)

* readme

* Add queue log

* Test interactive (#4509)

* Support nested node interaction (#4503)

* feat: Add a new InteractiveContext type and update InteractiveBasicType, adding an optional context property to support more complex interaction state management.

* feat: Enhance workflow interactivity by adding InteractiveContext support and updating dispatch logic to manage nested contexts and entry nodes more effectively.

* feat: Refactor dispatchWorkFlow to utilize InteractiveContext for improved context management

* feat: Enhance entry node resolution by adding validation for entryNodeIds and recursive search in InteractiveContext

* feat: Remove workflowDepth from InteractiveContext and update recovery logic to utilize parentContext for improved context management

* feat: Update getWorkflowEntryNodeIds to use lastInteractive for improved context handling in runtime nodes

* feat: Add lastInteractive support to enhance context management across workflow components

* feat: Enhance interactive workflow by adding stopForInteractive flag and improving memory edge validation in runtime logic

* feat: Refactor InteractiveContext by removing interactiveAppId and updating runtime edge handling in dispatchRunApp for improved context management

* feat: Simplify runtime node and edge initialization in dispatchRunApp by using ternary operators for improved readability and maintainability

* feat: Improve memory edge validation in initWorkflowEdgeStatus by adding detailed comments for better understanding of subset checks and recursive context searching

* feat: Remove commented-out current level information from InteractiveContext for cleaner code and improved readability

* feat: Simplify stopForInteractive check in dispatchWorkFlow for improved code clarity and maintainability

* feat: Remove stopForInteractive handling and related references for improved code clarity and maintainability

* feat: Add interactive response handling in dispatchRunAppNode for enhanced workflow interactivity

* feat: Add context property to InteractiveBasicType and InteractiveNodeType for improved interactivity management

* feat: remove comments

* feat: Remove the node property from ChatDispatchProps to simplify type definitions

* feat: Remove workflowInteractiveResponse from dispatchRunAppNode for cleaner code

* feat: Refactor interactive value handling in chat history processing for improved clarity

* feat: Simplify initWorkflowEdgeStatus logic for better readability and maintainability

* feat: Add workflowInteractiveResponse to dispatchWorkFlow for enhanced functionality

* feat: Enhance interactive response handling with nested children support

* feat: Remove commented-out code for interactive node handling to improve clarity

* feat: remove  InteractiveContext type

* feat: Refactor UserSelectInteractive and UserInputInteractive params for improved structure and clarity

* feat: remove

* feat: The front end supports extracting the deepest interaction parameters to enhance interaction processing

* feat: The front end supports extracting the deepest interaction parameters to enhance interaction processing

* fix: handle undefined interactive values in runtimeEdges and runtimeNodes initialization

* fix: handle undefined interactive values in runtimeNodes and runtimeEdges initialization

* fix: update runtimeNodes and runtimeEdges initialization to use last interactive value

* fix: remove unused imports and replace getLastInteractiveValue with lastInteractive in runtimeEdges initialization

* fix: import WorkflowInteractiveResponseType and handle lastInteractive as undefined in chatTest

* feat: implement extractDeepestInteractive function and refactor usage in AIResponseBox and ChatBox utils

* fix: refactor initWorkflowEdgeStatus and getWorkflowEntryNodeIds calls in dispatchRunAppNode for recovery handling

* fix: ensure lastInteractive is handled consistently as undefined in runtimeEdges and runtimeNodes initialization

* fix: update dispatchFormInput and dispatchUserSelect to use lastInteractive consistently

* fix: update condition checks in dispatchFormInput and dispatchUserSelect to ensure lastInteractive type is validated correctly

* fix: refactor dispatchRunAppNode to replace isRecovery with childrenInteractive for improved clarity in runtimeNodes and runtimeEdges initialization

* refactor: streamline runtimeNodes and runtimeEdges initialization in dispatchRunAppNode for improved readability and maintainability

* fix: update rewriteNodeOutputByHistories function to accept runtimeNodes and interactive as parameters for improved clarity

* fix: simplify interactiveResponse assignment in dispatchWorkFlow for improved clarity

* fix: update entryNodeIds check in getWorkflowEntryNodeIds to ensure it's an array for improved reliability

* remove some invalid code

---------

Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>

* update doc

* update log

* fix: update debug workflow to conditionally include nextStepSkipNodes… (#4511)

* fix: update debug workflow to conditionally include nextStepSkipNodes based on lastInteractive for improved debugging accuracy

* fix : type error

* remove invalid code

* fix: QA queue

* fix: interactive

* Test log (#4519)

* add log (#4504)

* add log

* update log i18n

* update log

* delete template

* add i18NT

* add team operation log

---------

Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com>

* remove search

* update doc

---------

Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com>
This commit is contained in:
Archer
2025-04-12 12:48:19 +08:00
committed by GitHub
parent b51a87f5b7
commit 16a22bc76a
34 changed files with 661 additions and 203 deletions

View File

@@ -10,7 +10,7 @@
<a href="./README_ja.md">日语</a>
</p>
FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景!
FastGPT 是一个 AI Agent 构建平台,提供开箱即用的数据处理、模型调用等能力同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的应用场景!
</div>

View File

@@ -11,10 +11,18 @@ weight: 795
## 🚀 新增内容
1. 团队成员权限细分,可分别控制是否可创建在根目录应用/知识库以及 API Key
2. 支持交互节点在嵌套工作流中使用。
3. 团队成员操作日志。
## ⚙️ 优化
1. 繁体中文翻译。
## 🐛 修复
1. password 检测规则错误
1. password 检测规则错误
2. 分享链接无法隐藏知识库检索结果。
3. IOS 低版本正则兼容问题。
4. 修复问答提取队列错误后,计数器未清零问题,导致问答提取队列失效。
5. Debug 模式交互节点下一步可能造成死循环。

View File

@@ -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<string, string>;
@@ -55,6 +55,7 @@ export type ChatDispatchProps = {
variables: Record<string, any>; // global variable
query: UserChatItemValueItemType[]; // trigger query
chatConfig: AppSchema['chatConfig'];
lastInteractive?: WorkflowInteractiveResponseType; // last interactive response
stream: boolean;
maxRunTimes: number;
isToolCall?: boolean;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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'
}

View File

@@ -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<string, string>;
};
export type OperationListItemType = {
_id: string;
sourceMember: SourceMemberType;
event: `${OperationLogEventEnum}`;
timestamp: Date;
metadata: Record<string, string>;
};

View File

@@ -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);
});
}
}

View File

@@ -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) {

View File

@@ -141,6 +141,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
} else {
props.workflowDispatchDeep += 1;
}
const isRootRuntime = props.workflowDispatchDeep === 1;
if (props.workflowDispatchDeep > 20) {
return {
@@ -161,25 +162,28 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
let workflowRunTimes = 0;
// set sse response headers
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');
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();
// 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<DispatchFlowRespons
});
if (props.mode === 'debug') {
debugNextStepRunNodes = debugNextStepRunNodes.concat([
...nextStepActiveNodes,
...nextStepSkipNodes
]);
debugNextStepRunNodes = debugNextStepRunNodes.concat(
props.lastInteractive ? nextStepActiveNodes : [...nextStepActiveNodes, ...nextStepSkipNodes]
);
return {
nextStepActiveNodes: [],
nextStepSkipNodes: []
@@ -374,7 +377,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
};
// Tool call, not need interactive response
if (!props.isToolCall) {
if (!props.isToolCall && isRootRuntime) {
props.workflowStreamResponse?.({
event: SseResponseEventEnum.interactive,
data: { interactive: interactiveResult }
@@ -428,14 +431,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
})();
if (!nodeRunResult) return [];
if (res?.closed) {
addLog.warn('Request is closed', {
appId: props.runningAppInfo.id,
nodeId: node.nodeId,
nodeName: node.name
});
return [];
}
/*
特殊情况:
@@ -492,6 +487,15 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
await Promise.all(nextStepSkipNodes.map((node) => 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<DispatchFlowRespons
if (
version === 'v2' &&
!props.isToolCall &&
!props.runningAppInfo.isChildApp &&
isRootRuntime &&
formatResponseData &&
!(!props.responseDetail && filterModuleTypeList.includes(formatResponseData.moduleType))
) {
@@ -721,7 +725,9 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
entryNodeIds: nodeInteractiveResponse.entryNodeIds,
interactiveResponse: nodeInteractiveResponse.interactiveResponse
});
chatAssistantResponse.push(interactiveAssistant);
if (isRootRuntime) {
chatAssistantResponse.push(interactiveAssistant);
}
return interactiveAssistant.interactive;
}
})();

View File

@@ -10,7 +10,6 @@ import type {
UserInputInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { addLog } from '../../../../common/system/log';
import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.description]: string;
@@ -29,13 +28,13 @@ export const dispatchFormInput = async (props: Props): Promise<FormInputResponse
histories,
node,
params: { description, userInputForms },
query
query,
lastInteractive
} = props;
const { isEntry } = node;
const interactive = getLastInteractiveValue(histories);
// Interactive node is not the entry node, return interactive result
if (!isEntry || interactive?.type !== 'userInput') {
if (!isEntry || lastInteractive?.type !== 'userInput') {
return {
[DispatchNodeResponseKeyEnum.interactive]: {
type: 'userInput',

View File

@@ -10,7 +10,6 @@ import type {
UserSelectOptionItemType
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.description]: string;
@@ -27,13 +26,13 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon
histories,
node,
params: { description, userSelectOptions },
query
query,
lastInteractive
} = props;
const { nodeId, isEntry } = node;
const interactive = getLastInteractiveValue(histories);
// Interactive node is not the entry node, return interactive result
if (!isEntry || interactive?.type !== 'userSelect') {
if (!isEntry || lastInteractive?.type !== 'userSelect') {
return {
[DispatchNodeResponseKeyEnum.interactive]: {
type: 'userSelect',

View File

@@ -18,6 +18,7 @@ import { authAppByTmbId } from '../../../../support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppVersionById } from '../../../app/version/controller';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { ChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string;
@@ -27,6 +28,7 @@ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.fileUrlList]?: string[];
}>;
type Response = DispatchNodeResultType<{
[DispatchNodeResponseKeyEnum.interactive]?: ChildrenInteractive;
[NodeOutputKeyEnum.answerText]: string;
[NodeOutputKeyEnum.history]: ChatItemType[];
}>;
@@ -36,6 +38,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
runningAppInfo,
histories,
query,
lastInteractive,
node: { pluginId: appId, version },
workflowStreamResponse,
params,
@@ -100,31 +103,41 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
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<Response> => {
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]: {

View File

@@ -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<T extends OperationLogEventEnum>({
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
})
);
}

View File

@@ -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;
};
};

View File

@@ -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<OperationLogSchema>(
OperationLogCollectionName,
OperationLogSchema
);

View File

@@ -104,8 +104,11 @@ export async function addSourceMember<T extends { tmbId: string }>({
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 }
};
})

View File

@@ -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"
}

View File

@@ -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}}】]"
}

View File

@@ -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": "待接受"
}

View File

@@ -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
}
}

View File

@@ -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 <RenderTool showAnimation={isChatting} tools={value.tools} />;
}
if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {
if (value.interactive.type === 'userSelect') {
return <RenderUserSelectInteractive interactive={value.interactive} />;
const finalInteractive = extractDeepestInteractive(value.interactive);
if (finalInteractive.type === 'userSelect') {
return <RenderUserSelectInteractive interactive={finalInteractive} />;
}
if (value.interactive?.type === 'userInput') {
return <RenderUserFormInteractive interactive={value.interactive} />;
if (finalInteractive.type === 'userInput') {
return <RenderUserFormInteractive interactive={finalInteractive} />;
}
}
return null;

View File

@@ -303,7 +303,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
})()}
</Td>
<Td maxW={'300px'}>
<VStack gap={0}>
<VStack gap={0} align="start">
<Box>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box>
<Box>
{member.updateTime

View File

@@ -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<string>('');
const {
data: operationLogs = [],
isLoading: loadingLogs,
ScrollData: LogScrollData
} = useScrollPagination(getOperationLogs, {
pageSize: 20,
refreshDeps: [searchKey],
throttleWait: 500,
debounceWait: 200
});
const isLoading = loadingLogs;
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox isLoading={isLoading} flex={'1 0 0'} overflow={'auto'}>
<LogScrollData>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:log_user')}
</Th>
<Th bgColor="myGray.100">{t('account_team:log_time')}</Th>
<Th bgColor="myGray.100">{t('account_team:log_type')}</Th>
<Th bgColor="myGray.100">{t('account_team:log_details')}</Th>
</Tr>
</Thead>
<Tbody>
{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 ? (
<Tr key={log._id} overflow={'unset'}>
<Td>
<UserBox
sourceMember={log.sourceMember}
fontSize="sm"
avatarSize="1rem"
spacing={0.5}
/>
</Td>
<Td>{formatTime2YMDHMS(log.timestamp)}</Td>
<Td>{t(i18nData.typeLabel)}</Td>
<Td>{t(i18nData.content, metadata as any) as string}</Td>
</Tr>
) : null;
})}
</Tbody>
</Table>
</TableContainer>
</LogScrollData>
</MyBox>
</>
);
}
export default OperationLogTable;

View File

@@ -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
});

View File

@@ -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 && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.operationLog && <OperationLogTable Tabs={Tabs} />}
</Box>
</Flex>
{invitelinkid && <HandleInviteModal invitelinkid={invitelinkid} />}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<any> {
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<any> {
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);
}
}

View File

@@ -35,6 +35,8 @@ const reduceQueueAndReturn = (delay = 0) => {
/* 索引生成队列。每导入一次,就是一个单独的线程 */
export async function generateVector(): Promise<any> {
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();

View File

@@ -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<PaginationProps>) =>
POST<PaginationResponse<OperationListItemType>>(
`/proApi/support/user/team/operationLog/list`,
props
);