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

View File

@@ -11,10 +11,18 @@ weight: 795
## 🚀 新增内容 ## 🚀 新增内容
1. 团队成员权限细分,可分别控制是否可创建在根目录应用/知识库以及 API Key 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 { AiChatQuoteRoleType } from '../template/system/aiChat/type';
import { LafAccountType, OpenaiAccountType } from '../../../support/user/team/type'; import { LafAccountType, OpenaiAccountType } from '../../../support/user/team/type';
import { CompletionFinishReason } from '../../ai/type'; import { CompletionFinishReason } from '../../ai/type';
import { WorkflowInteractiveResponseType } from '../template/system/interactive/type';
export type ExternalProviderType = { export type ExternalProviderType = {
openaiAccount?: OpenaiAccountType; openaiAccount?: OpenaiAccountType;
externalWorkflowVariables?: Record<string, string>; externalWorkflowVariables?: Record<string, string>;
@@ -55,6 +55,7 @@ export type ChatDispatchProps = {
variables: Record<string, any>; // global variable variables: Record<string, any>; // global variable
query: UserChatItemValueItemType[]; // trigger query query: UserChatItemValueItemType[]; // trigger query
chatConfig: AppSchema['chatConfig']; chatConfig: AppSchema['chatConfig'];
lastInteractive?: WorkflowInteractiveResponseType; // last interactive response
stream: boolean; stream: boolean;
maxRunTimes: number; maxRunTimes: number;
isToolCall?: boolean; isToolCall?: boolean;

View File

@@ -10,7 +10,19 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type'; import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
import { replaceVariable, valToStr } from '../../../common/string/tools'; 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 => { export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => {
let limit = 10; let limit = 10;
nodes.forEach((node) => { nodes.forEach((node) => {
@@ -34,7 +46,9 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
1. Get the interactive data 1. Get the interactive data
2. Check that the workflow starts at the interaction node 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); const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI);
if (lastAIMessage) { if (lastAIMessage) {
@@ -45,7 +59,11 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => {
lastValue.type !== ChatItemValueTypeEnum.interactive || lastValue.type !== ChatItemValueTypeEnum.interactive ||
!lastValue.interactive !lastValue.interactive
) { ) {
return null; return;
}
if (lastValue.interactive.type === 'childrenInteractive') {
return lastValue.interactive;
} }
// Check is user select // Check is user select
@@ -62,38 +80,29 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => {
} }
} }
return null; return;
}; };
export const initWorkflowEdgeStatus = ( export const initWorkflowEdgeStatus = (
edges: StoreEdgeItemType[] | RuntimeEdgeItemType[], edges: StoreEdgeItemType[],
histories?: ChatItemType[] lastInteractive?: WorkflowInteractiveResponseType
): RuntimeEdgeItemType[] => { ): RuntimeEdgeItemType[] => {
// If there is a history, use the last interactive value if (lastInteractive) {
if (histories && histories.length > 0) { const memoryEdges = lastInteractive.memoryEdges || [];
const memoryEdges = getLastInteractiveValue(histories)?.memoryEdges;
if (memoryEdges && memoryEdges.length > 0) { if (memoryEdges && memoryEdges.length > 0) {
return memoryEdges; return memoryEdges;
} }
} }
return ( return edges?.map((edge) => ({ ...edge, status: 'waiting' })) || [];
edges?.map((edge) => ({
...edge,
status: 'waiting'
})) || []
);
}; };
export const getWorkflowEntryNodeIds = ( export const getWorkflowEntryNodeIds = (
nodes: (StoreNodeItemType | RuntimeNodeItemType)[], nodes: (StoreNodeItemType | RuntimeNodeItemType)[],
histories?: ChatItemType[] lastInteractive?: WorkflowInteractiveResponseType
) => { ) => {
// If there is a history, use the last interactive entry node if (lastInteractive) {
if (histories && histories.length > 0) { const entryNodeIds = lastInteractive.entryNodeIds || [];
const entryNodeIds = getLastInteractiveValue(histories)?.entryNodeIds;
if (Array.isArray(entryNodeIds) && entryNodeIds.length > 0) { if (Array.isArray(entryNodeIds) && entryNodeIds.length > 0) {
return entryNodeIds; return entryNodeIds;
} }
@@ -396,10 +405,10 @@ export const textAdaptGptResponse = ({
/* Update runtimeNode's outputs with interactive data from history */ /* Update runtimeNode's outputs with interactive data from history */
export function rewriteNodeOutputByHistories( export function rewriteNodeOutputByHistories(
histories: ChatItemType[], runtimeNodes: RuntimeNodeItemType[],
runtimeNodes: RuntimeNodeItemType[] lastInteractive?: InteractiveNodeResponseType
) { ) {
const interactive = getLastInteractiveValue(histories); const interactive = lastInteractive;
if (!interactive?.nodeOutputs) { if (!interactive?.nodeOutputs) {
return runtimeNodes; return runtimeNodes;
} }

View File

@@ -1,6 +1,5 @@
import type { NodeOutputItemType } from '../../../../chat/type'; import type { NodeOutputItemType } from '../../../../chat/type';
import type { FlowNodeOutputItemType } from '../../../type/io'; import type { FlowNodeOutputItemType } from '../../../type/io';
import type { RuntimeEdgeItemType } from '../../../runtime/type';
import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant'; import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from 'core/workflow/constants'; import { WorkflowIOValueTypeEnum } from 'core/workflow/constants';
import type { ChatCompletionMessageParam } from '../../../../ai/type'; import type { ChatCompletionMessageParam } from '../../../../ai/type';
@@ -9,7 +8,6 @@ type InteractiveBasicType = {
entryNodeIds: string[]; entryNodeIds: string[];
memoryEdges: RuntimeEdgeItemType[]; memoryEdges: RuntimeEdgeItemType[];
nodeOutputs: NodeOutputItemType[]; nodeOutputs: NodeOutputItemType[];
toolParams?: { toolParams?: {
entryNodeIds: string[]; // 记录工具中,交互节点的 Id而不是起始工作流的入口 entryNodeIds: string[]; // 记录工具中,交互节点的 Id而不是起始工作流的入口
memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages
@@ -23,6 +21,13 @@ type InteractiveNodeType = {
nodeOutputs?: NodeOutputItemType[]; nodeOutputs?: NodeOutputItemType[];
}; };
type ChildrenInteractive = InteractiveNodeType & {
type: 'childrenInteractive';
params: {
childrenResponse?: WorkflowInteractiveResponseType;
};
};
export type UserSelectOptionItemType = { export type UserSelectOptionItemType = {
key: string; key: string;
value: string; value: string;
@@ -62,5 +67,9 @@ type UserInputInteractive = InteractiveNodeType & {
submitted?: boolean; submitted?: boolean;
}; };
}; };
export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive;
export type InteractiveNodeResponseType =
| UserSelectInteractive
| UserInputInteractive
| ChildrenInteractive;
export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType; 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 { PerConstructPros, Permission } from '../controller';
import { import {
TeamApikeyCreatePermissionVal,
TeamAppCreatePermissionVal, TeamAppCreatePermissionVal,
TeamDatasetCreatePermissionVal,
TeamDefaultPermissionVal, TeamDefaultPermissionVal,
TeamPermissionList TeamPermissionList
} from './constant'; } from './constant';
@@ -23,8 +25,8 @@ export class TeamPermission extends Permission {
this.setUpdatePermissionCallback(() => { this.setUpdatePermissionCallback(() => {
this.hasAppCreatePer = this.checkPer(TeamAppCreatePermissionVal); this.hasAppCreatePer = this.checkPer(TeamAppCreatePermissionVal);
this.hasDatasetCreatePer = this.checkPer(TeamAppCreatePermissionVal); this.hasDatasetCreatePer = this.checkPer(TeamDatasetCreatePermissionVal);
this.hasApikeyCreatePer = this.checkPer(TeamAppCreatePermissionVal); this.hasApikeyCreatePer = this.checkPer(TeamApikeyCreatePermissionVal);
}); });
} }
} }

View File

@@ -16,6 +16,7 @@ import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { pushChatLog } from './pushChatLog'; import { pushChatLog } from './pushChatLog';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
type Props = { type Props = {
chatId: string; chatId: string;
@@ -209,23 +210,15 @@ export const updateInteractiveChat = async ({
} }
})(); })();
if (interactiveValue.interactive.type === 'userSelect') { let finalInteractive = extractDeepestInteractive(interactiveValue.interactive);
interactiveValue.interactive = {
...interactiveValue.interactive, if (finalInteractive.type === 'userSelect') {
params: { finalInteractive.params.userSelectedVal = userInteractiveVal;
...interactiveValue.interactive.params,
userSelectedVal: userInteractiveVal
}
};
} else if ( } else if (
interactiveValue.interactive.type === 'userInput' && finalInteractive.type === 'userInput' &&
typeof parsedUserInteractiveVal === 'object' typeof parsedUserInteractiveVal === 'object'
) { ) {
interactiveValue.interactive = { finalInteractive.params.inputForm = finalInteractive.params.inputForm.map((item) => {
...interactiveValue.interactive,
params: {
...interactiveValue.interactive.params,
inputForm: interactiveValue.interactive.params.inputForm.map((item) => {
const itemValue = parsedUserInteractiveVal[item.label]; const itemValue = parsedUserInteractiveVal[item.label];
return itemValue !== undefined return itemValue !== undefined
? { ? {
@@ -233,10 +226,8 @@ export const updateInteractiveChat = async ({
value: itemValue value: itemValue
} }
: item; : item;
}), });
submitted: true finalInteractive.params.submitted = true;
}
};
} }
if (aiResponse.customFeedbacks) { if (aiResponse.customFeedbacks) {

View File

@@ -141,6 +141,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
} else { } else {
props.workflowDispatchDeep += 1; props.workflowDispatchDeep += 1;
} }
const isRootRuntime = props.workflowDispatchDeep === 1;
if (props.workflowDispatchDeep > 20) { if (props.workflowDispatchDeep > 20) {
return { return {
@@ -161,6 +162,8 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
let workflowRunTimes = 0; let workflowRunTimes = 0;
// set sse response headers // set sse response headers
if (isRootRuntime) {
res?.setHeader('Connection', 'keep-alive'); // Set keepalive for long connection
if (stream && res) { if (stream && res) {
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8'); res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
@@ -181,6 +184,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}; };
sendStreamTimerSign(); sendStreamTimerSign();
} }
}
variables = { variables = {
...getSystemVariable(data), ...getSystemVariable(data),
@@ -325,10 +329,9 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}); });
if (props.mode === 'debug') { if (props.mode === 'debug') {
debugNextStepRunNodes = debugNextStepRunNodes.concat([ debugNextStepRunNodes = debugNextStepRunNodes.concat(
...nextStepActiveNodes, props.lastInteractive ? nextStepActiveNodes : [...nextStepActiveNodes, ...nextStepSkipNodes]
...nextStepSkipNodes );
]);
return { return {
nextStepActiveNodes: [], nextStepActiveNodes: [],
nextStepSkipNodes: [] nextStepSkipNodes: []
@@ -374,7 +377,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}; };
// Tool call, not need interactive response // Tool call, not need interactive response
if (!props.isToolCall) { if (!props.isToolCall && isRootRuntime) {
props.workflowStreamResponse?.({ props.workflowStreamResponse?.({
event: SseResponseEventEnum.interactive, event: SseResponseEventEnum.interactive,
data: { interactive: interactiveResult } data: { interactive: interactiveResult }
@@ -428,14 +431,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
})(); })();
if (!nodeRunResult) return []; 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))) await Promise.all(nextStepSkipNodes.map((node) => checkNodeCanRun(node, skippedNodeIdList)))
).flat(); ).flat();
if (res?.closed) {
addLog.warn('Request is closed', {
appId: props.runningAppInfo.id,
nodeId: node.nodeId,
nodeName: node.name
});
return [];
}
return [ return [
...nextStepActiveNodes, ...nextStepActiveNodes,
...nextStepSkipNodes, ...nextStepSkipNodes,
@@ -632,7 +636,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
if ( if (
version === 'v2' && version === 'v2' &&
!props.isToolCall && !props.isToolCall &&
!props.runningAppInfo.isChildApp && isRootRuntime &&
formatResponseData && formatResponseData &&
!(!props.responseDetail && filterModuleTypeList.includes(formatResponseData.moduleType)) !(!props.responseDetail && filterModuleTypeList.includes(formatResponseData.moduleType))
) { ) {
@@ -721,7 +725,9 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
entryNodeIds: nodeInteractiveResponse.entryNodeIds, entryNodeIds: nodeInteractiveResponse.entryNodeIds,
interactiveResponse: nodeInteractiveResponse.interactiveResponse interactiveResponse: nodeInteractiveResponse.interactiveResponse
}); });
if (isRootRuntime) {
chatAssistantResponse.push(interactiveAssistant); chatAssistantResponse.push(interactiveAssistant);
}
return interactiveAssistant.interactive; return interactiveAssistant.interactive;
} }
})(); })();

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ import { authAppByTmbId } from '../../../../support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppVersionById } from '../../../app/version/controller'; import { getAppVersionById } from '../../../app/version/controller';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { ChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.userChatInput]: string;
@@ -27,6 +28,7 @@ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.fileUrlList]?: string[]; [NodeInputKeyEnum.fileUrlList]?: string[];
}>; }>;
type Response = DispatchNodeResultType<{ type Response = DispatchNodeResultType<{
[DispatchNodeResponseKeyEnum.interactive]?: ChildrenInteractive;
[NodeOutputKeyEnum.answerText]: string; [NodeOutputKeyEnum.answerText]: string;
[NodeOutputKeyEnum.history]: ChatItemType[]; [NodeOutputKeyEnum.history]: ChatItemType[];
}>; }>;
@@ -36,6 +38,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
runningAppInfo, runningAppInfo,
histories, histories,
query, query,
lastInteractive,
node: { pluginId: appId, version }, node: { pluginId: appId, version },
workflowStreamResponse, workflowStreamResponse,
params, params,
@@ -100,8 +103,21 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
appId: String(appData._id) appId: String(appData._id)
}; };
const { flowResponses, flowUsages, assistantResponses, runTimes } = await dispatchWorkFlow({ 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, ...props,
lastInteractive: childrenInteractive,
// Rewrite stream mode // Rewrite stream mode
...(system_forbid_stream ...(system_forbid_stream
? { ? {
@@ -115,14 +131,11 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
tmbId: String(appData.tmbId), tmbId: String(appData.tmbId),
isChildApp: true isChildApp: true
}, },
runtimeNodes: storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)), runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges), runtimeEdges,
histories: chatHistories, histories: chatHistories,
variables: childrenRunVariables, variables: childrenRunVariables,
query: runtimePrompt2ChatsValue({ query: theQuery,
files: userInputFiles,
text: userChatInput
}),
chatConfig chatConfig
}); });
@@ -142,6 +155,14 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
const usagePoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0); const usagePoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0);
return { return {
[DispatchNodeResponseKeyEnum.interactive]: workflowInteractiveResponse
? {
type: 'childrenInteractive',
params: {
childrenResponse: workflowInteractiveResponse
}
}
: undefined,
assistantResponses: system_forbid_stream ? [] : assistantResponses, assistantResponses: system_forbid_stream ? [] : assistantResponses,
[DispatchNodeResponseKeyEnum.runTimes]: runTimes, [DispatchNodeResponseKeyEnum.runTimes]: runTimes,
[DispatchNodeResponseKeyEnum.nodeResponse]: { [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)); const tmb = tmbList.find((tmb) => String(tmb._id) === String(item.tmbId));
if (!tmb) return; if (!tmb) return;
// @ts-ignore
const formatItem = typeof item.toObject === 'function' ? item.toObject() : item;
return { return {
...item, ...formatItem,
sourceMember: { name: tmb.name, avatar: tmb.avatar, status: tmb.status } sourceMember: { name: tmb.name, avatar: tmb.avatar, status: tmb.status }
}; };
}) })

View File

@@ -5,17 +5,23 @@
"7days": "7 Days", "7days": "7 Days",
"accept": "accept", "accept": "accept",
"action": "operate", "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_group": "Confirm to delete group?",
"confirm_delete_member": "Confirm to delete member?", "confirm_delete_member": "Confirm to delete member?",
"confirm_delete_org": "Confirm to delete organization?", "confirm_delete_org": "Confirm to delete organization?",
"confirm_forbidden": "Confirm forbidden", "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.", "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", "copy_link": "Copy link",
"create_department": "Create a sub-department",
"create_group": "Create group", "create_group": "Create group",
"create_invitation_link": "Create Invitation Link", "create_invitation_link": "Create Invitation Link",
"create_org": "Create organization", "create_org": "Create organization",
"create_sub_org": "Create sub-organization", "create_sub_org": "Create sub-organization",
"delete": "delete", "delete": "delete",
"delete_department": "Delete sub-department",
"delete_group": "Delete a group",
"delete_org": "Delete organization", "delete_org": "Delete organization",
"edit_info": "Edit information", "edit_info": "Edit information",
"edit_member": "Edit user", "edit_member": "Edit user",
@@ -37,21 +43,51 @@
"invitation_link_list": "Invitation link list", "invitation_link_list": "Invitation link list",
"invite_member": "Invite members", "invite_member": "Invite members",
"invited": "Invited", "invited": "Invited",
"join_team": "Join the team",
"kick_out_team": "Remove members",
"label_sync": "Tag sync", "label_sync": "Tag sync",
"leave_team_failed": "Leaving the team exception", "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", "manage_member": "Managing members",
"member": "member", "member": "member",
"member_group": "Belonging to member group", "member_group": "Belonging to member group",
"move_member": "Move member", "move_member": "Move member",
"move_org": "Move organization", "move_org": "Move organization",
"operation_log": "log",
"org": "organization", "org": "organization",
"org_description": "Organization description", "org_description": "Organization description",
"org_name": "Organization name", "org_name": "Organization name",
"owner": "owner", "owner": "owner",
"permission": "Permissions", "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", "remark": "remark",
"remove_tip": "Confirm to remove {{username}} from the team?", "remove_tip": "Confirm to remove {{username}} from the team?",
"retain_admin_permissions": "Keep administrator rights", "retain_admin_permissions": "Keep administrator rights",
"search_log": "Search log",
"search_member_group_name": "Search member/group name", "search_member_group_name": "Search member/group name",
"total_team_members": "{{amount}} members in total", "total_team_members": "{{amount}} members in total",
"transfer_ownership": "transfer owner", "transfer_ownership": "transfer owner",
@@ -61,13 +97,5 @@
"user_team_invite_member": "Invite members", "user_team_invite_member": "Invite members",
"user_team_leave_team": "Leave the team", "user_team_leave_team": "Leave the team",
"user_team_leave_team_failed": "Failure to leave the team", "user_team_leave_team_failed": "Failure to leave the team",
"waiting": "To be accepted", "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"
} }

View File

@@ -5,6 +5,9 @@
"7days": "7天", "7days": "7天",
"accept": "接受", "accept": "接受",
"action": "操作", "action": "操作",
"assign_permission": "权限变更",
"change_department_name": "部门编辑",
"change_member_name": "成员改名",
"confirm_delete_from_org": "确认将 {{username}} 移出部门?", "confirm_delete_from_org": "确认将 {{username}} 移出部门?",
"confirm_delete_from_team": "确认将 {{username}} 移出团队?", "confirm_delete_from_team": "确认将 {{username}} 移出团队?",
"confirm_delete_group": "确认删除群组?", "confirm_delete_group": "确认删除群组?",
@@ -12,13 +15,16 @@
"confirm_forbidden": "确认停用", "confirm_forbidden": "确认停用",
"confirm_leave_team": "确认离开该团队? \n退出后您在该团队所有的资源均转让给团队所有者。", "confirm_leave_team": "确认离开该团队? \n退出后您在该团队所有的资源均转让给团队所有者。",
"copy_link": "复制链接", "copy_link": "复制链接",
"create_department": "创建子部门",
"create_group": "创建群组", "create_group": "创建群组",
"create_invitation_link": "创建邀请链接", "create_invitation_link": "创建邀请链接",
"create_org": "创建部门", "create_org": "创建部门",
"create_sub_org": "创建子部门", "create_sub_org": "创建子部门",
"delete": "删除", "delete": "删除",
"delete_department": "删除子部门",
"delete_from_org": "移出部门", "delete_from_org": "移出部门",
"delete_from_team": "移出团队", "delete_from_team": "移出团队",
"delete_group": "删除群组",
"delete_org": "删除部门", "delete_org": "删除部门",
"edit_info": "编辑信息", "edit_info": "编辑信息",
"edit_member": "编辑用户", "edit_member": "编辑用户",
@@ -41,27 +47,37 @@
"invitation_link_list": "链接列表", "invitation_link_list": "链接列表",
"invite_member": "邀请成员", "invite_member": "邀请成员",
"invited": "已邀请", "invited": "已邀请",
"join_team": "加入团队",
"join_update_time": "加入/更新时间", "join_update_time": "加入/更新时间",
"kick_out_team": "移除成员",
"label_sync": "标签同步", "label_sync": "标签同步",
"leave": "已离职", "leave": "已离职",
"leave_team_failed": "离开团队异常", "leave_team_failed": "离开团队异常",
"log_details": "详情",
"log_time": "操作时间",
"log_type": "操作类型",
"log_user": "操作人员",
"login": "登录",
"manage_member": "管理成员", "manage_member": "管理成员",
"member": "成员", "member": "成员",
"member_group": "所属群组", "member_group": "所属群组",
"move_member": "移动成员", "move_member": "移动成员",
"move_org": "移动部门", "move_org": "移动部门",
"notification_recieve": "团队通知接收", "notification_recieve": "团队通知接收",
"operation_log": "日志",
"org": "部门", "org": "部门",
"org_description": "介绍", "org_description": "介绍",
"org_name": "部门名称", "org_name": "部门名称",
"owner": "所有者", "owner": "所有者",
"permission": "权限", "permission": "权限",
"please_bind_contact": "请绑定联系方式", "please_bind_contact": "请绑定联系方式",
"relocate_department": "部门移动",
"remark": "备注", "remark": "备注",
"remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“已离职”,不删除操作数据,账号下资源自动转让给团队所有者。", "remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“已离职”,不删除操作数据,账号下资源自动转让给团队所有者。",
"restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。", "restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。",
"restore_tip_title": "恢复确认", "restore_tip_title": "恢复确认",
"retain_admin_permissions": "保留管理员权限", "retain_admin_permissions": "保留管理员权限",
"search_log": "搜索日志",
"search_member": "搜索成员", "search_member": "搜索成员",
"search_member_group_name": "搜索成员/群组名称", "search_member_group_name": "搜索成员/群组名称",
"search_org": "搜索部门", "search_org": "搜索部门",
@@ -85,5 +101,17 @@
"permission_datasetCreate_Tip": "可以在根目录创建知识库,(文件夹下的创建权限由文件夹控制)", "permission_datasetCreate_Tip": "可以在根目录创建知识库,(文件夹下的创建权限由文件夹控制)",
"permission_apikeyCreate_Tip": "可以创建全局的 APIKey", "permission_apikeyCreate_Tip": "可以创建全局的 APIKey",
"permission_manage": "管理员", "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 天", "7days": "7 天",
"accept": "接受", "accept": "接受",
"action": "操作", "action": "操作",
"assign_permission": "權限變更",
"change_department_name": "部門編輯",
"change_member_name": "成員改名",
"confirm_delete_group": "確認刪除群組?", "confirm_delete_group": "確認刪除群組?",
"confirm_delete_member": "確認刪除成員?", "confirm_delete_member": "確認刪除成員?",
"confirm_delete_org": "確認刪除該部門?", "confirm_delete_org": "確認刪除該部門?",
"confirm_forbidden": "確認停用", "confirm_forbidden": "確認停用",
"confirm_leave_team": "確認離開該團隊? \n結束後您在該團隊所有的資源轉讓給團隊所有者。", "confirm_leave_team": "確認離開該團隊? \n結束後您在該團隊所有的資源轉讓給團隊所有者。",
"copy_link": "複製連結", "copy_link": "複製連結",
"create_department": "創建子部門",
"create_group": "建立群組", "create_group": "建立群組",
"create_invitation_link": "建立邀請連結", "create_invitation_link": "建立邀請連結",
"create_org": "建立部門", "create_org": "建立部門",
"create_sub_org": "建立子部門", "create_sub_org": "建立子部門",
"delete": "刪除", "delete": "刪除",
"delete_department": "刪除子部門",
"delete_group": "刪除群組",
"delete_org": "刪除部門", "delete_org": "刪除部門",
"edit_info": "編輯訊息", "edit_info": "編輯訊息",
"edit_member": "編輯使用者", "edit_member": "編輯使用者",
@@ -37,21 +43,51 @@
"invitation_link_list": "連結列表", "invitation_link_list": "連結列表",
"invite_member": "邀請成員", "invite_member": "邀請成員",
"invited": "已邀請", "invited": "已邀請",
"join_team": "加入團隊",
"kick_out_team": "移除成員",
"label_sync": "標籤同步", "label_sync": "標籤同步",
"leave_team_failed": "離開團隊異常", "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": "管理成員", "manage_member": "管理成員",
"member": "成員", "member": "成員",
"member_group": "所屬成員組", "member_group": "所屬成員組",
"move_member": "移動成員", "move_member": "移動成員",
"move_org": "行動部門", "move_org": "行動部門",
"operation_log": "紀錄",
"org": "組織", "org": "組織",
"org_description": "介紹", "org_description": "介紹",
"org_name": "部門名稱", "org_name": "部門名稱",
"owner": "擁有者", "owner": "擁有者",
"permission": "權限", "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": "備註", "remark": "備註",
"remove_tip": "確認將 {{username}} 移出團隊?", "remove_tip": "確認將 {{username}} 移出團隊?",
"retain_admin_permissions": "保留管理員權限", "retain_admin_permissions": "保留管理員權限",
"search_log": "搜索日誌",
"search_member_group_name": "搜尋成員/群組名稱", "search_member_group_name": "搜尋成員/群組名稱",
"total_team_members": "共 {{amount}} 名成員", "total_team_members": "共 {{amount}} 名成員",
"transfer_ownership": "轉讓所有者", "transfer_ownership": "轉讓所有者",
@@ -61,13 +97,5 @@
"user_team_invite_member": "邀請成員", "user_team_invite_member": "邀請成員",
"user_team_leave_team": "離開團隊", "user_team_leave_team": "離開團隊",
"user_team_leave_team_failed": "離開團隊失敗", "user_team_leave_team_failed": "離開團隊失敗",
"waiting": "待接受", "waiting": "待接受"
"permission_appCreate": "建立應用程式",
"permission_datasetCreate": "建立知識庫",
"permission_apikeyCreate": "建立 API 密鑰",
"permission_appCreate_tip": "可以在根目錄建立應用程式,(資料夾下的建立權限由資料夾控制)",
"permission_datasetCreate_Tip": "可以在根目錄建立知識庫,(資料夾下的建立權限由資料夾控制)",
"permission_apikeyCreate_Tip": "可以建立全域的 APIKey",
"permission_manage": "管理員",
"permission_manage_tip": "可以管理成員、建立群組、管理所有群組、為群組和成員分配權限"
} }

View File

@@ -6,6 +6,7 @@ import {
import { ChatBoxInputType, UserInputFileItemType } from './type'; import { ChatBoxInputType, UserInputFileItemType } from './type';
import { getFileIcon } from '@fastgpt/global/common/file/icon'; import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { ChatItemValueTypeEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => { export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
if (!value) { if (!value) {
@@ -82,17 +83,19 @@ export const setUserSelectResultToHistories = (
i !== item.value.length - 1 || i !== item.value.length - 1 ||
val.type !== ChatItemValueTypeEnum.interactive || val.type !== ChatItemValueTypeEnum.interactive ||
!val.interactive !val.interactive
) ) {
return val; return val;
}
if (val.interactive.type === 'userSelect') { const finalInteractive = extractDeepestInteractive(val.interactive);
if (finalInteractive.type === 'userSelect') {
return { return {
...val, ...val,
interactive: { interactive: {
...val.interactive, ...finalInteractive,
params: { params: {
...val.interactive.params, ...finalInteractive.params,
userSelectedVal: val.interactive.params.userSelectOptions.find( userSelectedVal: finalInteractive.params.userSelectOptions.find(
(item) => item.value === interactiveVal (item) => item.value === interactiveVal
)?.value )?.value
} }
@@ -100,13 +103,13 @@ export const setUserSelectResultToHistories = (
}; };
} }
if (val.interactive.type === 'userInput') { if (finalInteractive.type === 'userInput') {
return { return {
...val, ...val,
interactive: { interactive: {
...val.interactive, ...finalInteractive,
params: { params: {
...val.interactive.params, ...finalInteractive.params,
submitted: true submitted: true
} }
} }

View File

@@ -28,6 +28,7 @@ import { isEqual } from 'lodash';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus'; import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents'; import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
const accordionButtonStyle = { const accordionButtonStyle = {
w: 'auto', w: 'auto',
@@ -245,11 +246,12 @@ const AIResponseBox = ({
return <RenderTool showAnimation={isChatting} tools={value.tools} />; return <RenderTool showAnimation={isChatting} tools={value.tools} />;
} }
if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) { if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {
if (value.interactive.type === 'userSelect') { const finalInteractive = extractDeepestInteractive(value.interactive);
return <RenderUserSelectInteractive interactive={value.interactive} />; if (finalInteractive.type === 'userSelect') {
return <RenderUserSelectInteractive interactive={finalInteractive} />;
} }
if (value.interactive?.type === 'userInput') { if (finalInteractive.type === 'userInput') {
return <RenderUserFormInteractive interactive={value.interactive} />; return <RenderUserFormInteractive interactive={finalInteractive} />;
} }
} }
return null; return null;

View File

@@ -303,7 +303,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
})()} })()}
</Td> </Td>
<Td maxW={'300px'}> <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>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box>
<Box> <Box>
{member.updateTime {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 SelectOptionsComponent
} from '@/components/core/chat/components/Interactive/InteractiveComponents'; } from '@/components/core/chat/components/Interactive/InteractiveComponents';
import { UserInputInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; 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 { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
@@ -130,10 +133,11 @@ const NodeDebugResponse = ({ nodeId, debugResult }: NodeDebugResponseProps) => {
} }
]; ];
const lastInteractive = getLastInteractiveValue(mockHistory);
onNextNodeDebug({ onNextNodeDebug({
...workflowDebugData, ...workflowDebugData,
// Rewrite runtimeEdges // Rewrite runtimeEdges
runtimeEdges: initWorkflowEdgeStatus(workflowDebugData.runtimeEdges, mockHistory), runtimeEdges: initWorkflowEdgeStatus(workflowDebugData.runtimeEdges, lastInteractive),
query: updatedQuery, query: updatedQuery,
history: mockHistory history: mockHistory
}); });

View File

@@ -18,6 +18,7 @@ const MemberTable = dynamic(() => import('@/pageComponents/account/team/MemberTa
const PermissionManage = dynamic( const PermissionManage = dynamic(
() => import('@/pageComponents/account/team/PermissionManage/index') () => 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 GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index'));
const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index')); const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index'));
const HandleInviteModal = dynamic( const HandleInviteModal = dynamic(
@@ -28,7 +29,8 @@ export enum TeamTabEnum {
member = 'member', member = 'member',
org = 'org', org = 'org',
group = 'group', group = 'group',
permission = 'permission' permission = 'permission',
operationLog = 'operationLog'
} }
const Team = () => { const Team = () => {
@@ -57,7 +59,8 @@ const Team = () => {
{ label: t('account_team:member'), value: TeamTabEnum.member }, { label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:org'), value: TeamTabEnum.org }, { label: t('account_team:org'), value: TeamTabEnum.org },
{ label: t('account_team:group'), value: TeamTabEnum.group }, { 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'} px={'1rem'}
value={teamTab} value={teamTab}
@@ -150,6 +153,7 @@ const Team = () => {
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />} {teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />} {teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />} {teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.operationLog && <OperationLogTable Tabs={Tabs} />}
</Box> </Box>
</Flex> </Flex>
{invitelinkid && <HandleInviteModal invitelinkid={invitelinkid} />} {invitelinkid && <HandleInviteModal invitelinkid={invitelinkid} />}

View File

@@ -98,7 +98,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const isPlugin = app.type === AppTypeEnum.plugin; const isPlugin = app.type === AppTypeEnum.plugin;
const userQuestion: UserChatItemType = (() => { const userQuestion: UserChatItemType = await (async () => {
if (isPlugin) { if (isPlugin) {
return getPluginRunUserQuery({ return getPluginRunUserQuery({
pluginInputs: getPluginInputsFromStoreNodes(app.modules), 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) { if (!latestHumanChat) {
throw new Error('User question is empty'); return Promise.reject('User question is empty');
} }
return latestHumanChat; return latestHumanChat;
})(); })();
@@ -136,14 +136,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} }
const newHistories = concatHistories(histories, chatMessages); const newHistories = concatHistories(histories, chatMessages);
const interactive = getLastInteractiveValue(newHistories) || undefined;
// Get runtimeNodes // Get runtimeNodes
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, interactive));
if (isPlugin) { if (isPlugin) {
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
variables = {}; variables = {};
} }
runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes); runtimeNodes = rewriteNodeOutputByHistories(runtimeNodes, interactive);
const workflowResponseWrite = getWorkflowResponseWrite({ const workflowResponseWrite = getWorkflowResponseWrite({
res, res,
@@ -175,9 +175,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId, chatId,
responseChatItemId, responseChatItemId,
runtimeNodes, runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges, newHistories), runtimeEdges: initWorkflowEdgeStatus(edges, interactive),
variables, variables,
query: removeEmptyUserInput(userQuestion.value), query: removeEmptyUserInput(userQuestion.value),
lastInteractive: interactive,
chatConfig, chatConfig,
histories: newHistories, histories: newHistories,
stream: true, stream: true,

View File

@@ -10,6 +10,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { defaultApp } from '@/web/core/app/constants'; import { defaultApp } from '@/web/core/app/constants';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils';
async function handler( async function handler(
req: NextApiRequest, req: NextApiRequest,
@@ -44,6 +45,7 @@ async function handler(
// auth balance // auth balance
const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(tmbId); const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(tmbId);
const lastInteractive = getLastInteractiveValue(history);
/* start process */ /* start process */
const { flowUsages, flowResponses, debugResponse, newVariables, workflowInteractiveResponse } = const { flowUsages, flowResponses, debugResponse, newVariables, workflowInteractiveResponse } =
@@ -65,6 +67,7 @@ async function handler(
}, },
runtimeNodes: nodes, runtimeNodes: nodes,
runtimeEdges: edges, runtimeEdges: edges,
lastInteractive,
variables, variables,
query: query, query: query,
chatConfig: defaultApp.chatConfig, 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 { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { UserErrEnum } from '@fastgpt/global/common/error/code/user'; 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) { async function handler(req: NextApiRequest, res: NextApiResponse) {
const { username, password } = req.body as PostLoginProps; const { username, password } = req.body as PostLoginProps;
@@ -64,6 +66,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
setCookie(res, token); setCookie(res, token);
addOperationLog({
tmbId: userDetail.team.tmbId,
teamId: userDetail.team.teamId,
event: OperationLogEventEnum.LOGIN
});
return { return {
user: userDetail, user: userDetail,
token token

View File

@@ -139,7 +139,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Computed start hook params // Computed start hook params
const startHookText = (() => { const startHookText = (() => {
// Chat // 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; if (userQuestion) return chatValue2RuntimePrompt(userQuestion.value).text;
// plugin // plugin
@@ -245,16 +245,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Get chat histories // Get chat histories
const newHistories = concatHistories(histories, chatMessages); const newHistories = concatHistories(histories, chatMessages);
const interactive = getLastInteractiveValue(newHistories) || undefined;
// Get runtimeNodes // Get runtimeNodes
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, interactive));
if (isPlugin) { if (isPlugin) {
// Assign values to runtimeNodes using variables // Assign values to runtimeNodes using variables
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
// Plugin runtime does not need global variables(It has been injected into the pluginInputNode) // Plugin runtime does not need global variables(It has been injected into the pluginInputNode)
variables = {}; variables = {};
} }
runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes); runtimeNodes = rewriteNodeOutputByHistories(runtimeNodes, interactive);
const workflowResponseWrite = getWorkflowResponseWrite({ const workflowResponseWrite = getWorkflowResponseWrite({
res, res,
@@ -288,7 +289,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId, chatId,
responseChatItemId, responseChatItemId,
runtimeNodes, runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges, newHistories), runtimeEdges: initWorkflowEdgeStatus(edges, interactive),
variables, variables,
query: removeEmptyUserInput(userQuestion.value), query: removeEmptyUserInput(userQuestion.value),
chatConfig, chatConfig,

View File

@@ -139,7 +139,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Computed start hook params // Computed start hook params
const startHookText = (() => { const startHookText = (() => {
// Chat // 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; if (userQuestion) return chatValue2RuntimePrompt(userQuestion.value).text;
// plugin // plugin
@@ -245,16 +245,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Get chat histories // Get chat histories
const newHistories = concatHistories(histories, chatMessages); const newHistories = concatHistories(histories, chatMessages);
const interactive = getLastInteractiveValue(newHistories) || undefined;
// Get runtimeNodes // Get runtimeNodes
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, interactive));
if (isPlugin) { if (isPlugin) {
// Assign values to runtimeNodes using variables // Assign values to runtimeNodes using variables
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
// Plugin runtime does not need global variables(It has been injected into the pluginInputNode) // Plugin runtime does not need global variables(It has been injected into the pluginInputNode)
variables = {}; variables = {};
} }
runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes); runtimeNodes = rewriteNodeOutputByHistories(runtimeNodes, interactive);
const workflowResponseWrite = getWorkflowResponseWrite({ const workflowResponseWrite = getWorkflowResponseWrite({
res, res,
@@ -288,9 +288,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId, chatId,
responseChatItemId, responseChatItemId,
runtimeNodes, runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges, newHistories), runtimeEdges: initWorkflowEdgeStatus(edges, interactive),
variables, variables,
query: removeEmptyUserInput(userQuestion.value), query: removeEmptyUserInput(userQuestion.value),
lastInteractive: interactive,
chatConfig, chatConfig,
histories: newHistories, histories: newHistories,
stream, stream,

View File

@@ -33,9 +33,21 @@ const reduceQueue = () => {
return global.qaQueueLen === 0; return global.qaQueueLen === 0;
}; };
const reduceQueueAndReturn = (delay = 0) => {
reduceQueue();
if (delay) {
setTimeout(() => {
generateQA();
}, delay);
} else {
generateQA();
}
};
export async function generateQA(): Promise<any> { export async function generateQA(): Promise<any> {
const max = global.systemEnv?.qaMaxProcess || 10; const max = global.systemEnv?.qaMaxProcess || 10;
addLog.debug(`[QA Queue] Queue size: ${global.qaQueueLen}`);
if (global.qaQueueLen >= max) return; if (global.qaQueueLen >= max) return;
global.qaQueueLen++; global.qaQueueLen++;
@@ -98,14 +110,12 @@ export async function generateQA(): Promise<any> {
return; return;
} }
if (error) { if (error) {
reduceQueue(); return reduceQueueAndReturn();
return generateQA();
} }
// auth balance // auth balance
if (!(await checkTeamAiPointsAndLock(data.teamId))) { if (!(await checkTeamAiPointsAndLock(data.teamId))) {
reduceQueue(); return reduceQueueAndReturn();
return generateQA();
} }
addLog.info(`[QA Queue] Start`); addLog.info(`[QA Queue] Start`);
@@ -137,14 +147,8 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
const qaArr = formatSplitText({ answer, rawText: text, llmModel: modelData }); // 格式化后的QA对 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 // get vector and insert
const { insertLen } = await pushDataListToTrainingQueueByCollectionId({ await pushDataListToTrainingQueueByCollectionId({
teamId: data.teamId, teamId: data.teamId,
tmbId: data.tmbId, tmbId: data.tmbId,
collectionId: data.collectionId, collectionId: data.collectionId,
@@ -160,7 +164,6 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
await MongoDatasetTraining.findByIdAndDelete(data._id); await MongoDatasetTraining.findByIdAndDelete(data._id);
// add bill // add bill
if (insertLen > 0) {
pushQAUsage({ pushQAUsage({
teamId: data.teamId, teamId: data.teamId,
tmbId: data.tmbId, tmbId: data.tmbId,
@@ -169,12 +172,13 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
billId: data.billId, billId: data.billId,
model: modelData.model model: modelData.model
}); });
} else { addLog.info(`[QA Queue] Finish`, {
addLog.info(`QA result 0:`, { answer }); time: Date.now() - startTime,
} splitLength: qaArr.length,
usage: chatResponse.usage
});
reduceQueue(); return reduceQueueAndReturn();
generateQA();
} catch (err: any) { } catch (err: any) {
addLog.error(`[QA Queue] Error`, err); addLog.error(`[QA Queue] Error`, err);
await MongoDatasetTraining.updateOne( await MongoDatasetTraining.updateOne(
@@ -188,9 +192,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
} }
); );
setTimeout(() => { return reduceQueueAndReturn(1000);
generateQA();
}, 1000);
} }
} }

View File

@@ -35,6 +35,8 @@ const reduceQueueAndReturn = (delay = 0) => {
/* 索引生成队列。每导入一次,就是一个单独的线程 */ /* 索引生成队列。每导入一次,就是一个单独的线程 */
export async function generateVector(): Promise<any> { export async function generateVector(): Promise<any> {
const max = global.systemEnv?.vectorMaxProcess || 10; const max = global.systemEnv?.vectorMaxProcess || 10;
addLog.debug(`[Vector Queue] Queue size: ${global.vectorQueueLen}`);
if (global.vectorQueueLen >= max) return; if (global.vectorQueueLen >= max) return;
global.vectorQueueLen++; global.vectorQueueLen++;
const start = Date.now(); 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
);