Compare commits

..

14 Commits

Author SHA1 Message Date
Archer
caa0755d9a perf: global variable any type (#1387) 2024-05-07 18:54:32 +08:00
Archer
fef1a1702b 4.8 test fix (#1385)
* fix: tool name cannot startwith number

* fix: chatbox update

* fix: chatbox

* perf: drag ui

* perf: drag component

* drag component
2024-05-07 18:41:34 +08:00
heheer
2a99e46353 fix: if else node (#1383)
* fix: if else node

* fix

* fix
2024-05-07 17:16:33 +08:00
Archer
8f9203c053 4.8 test (#1382)
* perf: some log, chatTest histories slice; http request failed tip

* fix: ssr render

* perf: if else node ui and fix value type select
2024-05-07 15:27:05 +08:00
heheer
2053bbdb1b feat: add elseif to ifelse node (#1378) 2024-05-07 14:50:58 +08:00
Archer
9e192c6d11 Ai histories (#1376)
* perf: workflow node ui

* i18n

* rename controller

* fix: zindex

* fix: leave page callback

* revert button
2024-05-07 13:32:01 +08:00
Archer
eef609a063 Fix 4.8 node (#1370)
* perf: runtime props

* fix: Plugin run faied in debug mode

* perf: variable update

* fix: ts

* perf: variable ui
2024-05-06 17:13:50 +08:00
heheer
5bb9c550f6 fix: add judge to update variable node input (#1369) 2024-05-06 15:54:15 +08:00
Archer
db1c27cdc7 feat: adapt v1 system plugin (#1366) 2024-05-06 15:21:29 +08:00
heheer
8863337606 fix: reference input of updateVariable node (#1367)
* fix: reference input of updateVariable node

* fix
2024-05-06 13:51:15 +08:00
heheer
59bd2a47b6 feat: add update variable node (#1362)
* feat: add variable update node

* fix

* fix

* change component quote
2024-05-06 12:20:29 +08:00
Archer
d057ba29f0 feat: custom feedback plugin (#1365) 2024-05-06 11:01:50 +08:00
Archer
b500631a4d fix: leave user will can not login (#1345) 2024-05-06 10:41:01 +08:00
Carson Yang
bf6084da69 Enhance English language support in i18n (#1348)
Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
2024-05-02 08:35:59 +08:00
98 changed files with 4546 additions and 2152 deletions

View File

@@ -6,10 +6,10 @@
"i18n-ally.localesPaths": [
"projects/app/i18n",
],
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.enabledParsers": ["json", "yaml", "js", "ts"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.keepFulfilled": false,
"i18n-ally.sourceLanguage": "zh", // 根据此语言文件翻译其他语言文件的变量和内容
"i18n-ally.displayLanguage": "zh", // 显示语言
"i18n-ally.displayLanguage": "zh" // 显示语言
}

View File

@@ -17,8 +17,6 @@ images: []
4. 无法解决时,可以找找[Issue](https://github.com/labring/FastGPT/issues),或新提 Issue私有部署错误务必提供详细的日志否则很难排查。
## 二、通用问题
### 能否纯本地运行
@@ -47,7 +45,7 @@ images: []
### 模型响应为空(core.chat.Chat API is error or undefined)
1. 检查 key 问题。
1. 检查 key 问题。curl 请求看是否正常。务必用 stream=true 模式。并且 maxToken 等相关参数尽量一致。
2. 如果是国内模型,可能是命中风控了。
3. 查看模型请求日志,检查出入参数是否异常。

View File

@@ -1,5 +1,5 @@
---
title: 'V4.8(进行中)'
title: 'V4.8(开发中)'
description: 'FastGPT V4.8 更新说明'
icon: 'upgrade'
draft: false
@@ -18,10 +18,14 @@ FastGPT workflow V2上线支持更加简洁的工作流模式。
## V4.8 更新说明
1. 重构 - 工作流
2. 新增 - 工作流 Debug 模式,可以调试单个节点或者逐步调试工作流
3. 新增 - 定时执行应用。可轻松实现定时任务
4. 新增 - 插件自定义输入优化,可以渲染输入组件
6. 优化 - 工作流连线,可以四向连接,方便构建循环工作流
7. 优化 - 工作流上下文传递,性能🚀
8. 优化 - 简易模式,更新配置后自动更新调试框内容,无需保存
9. 优化 - worker进程管理并将计算 Token 任务分配给 worker 进程。
2. 新增 - 判断器。支持 if elseIf else 判断
3. 新增 - 变量更新节点。支持更新运行中工作流输出变量,或更新全局变量
4. 新增 - 工作流 Debug 模式,可以调试单个节点或者逐步调试工作流
5. 新增 - 定时执行应用。可轻松实现定时任务
6. 新增 - 插件自定义输入优化,可以渲染输入组件
7. 优化 - 工作流连线,可以四向连接,方便构建循环工作流
8. 优化 - 工作流上下文传递,性能🚀。
9. 优化 - 简易模式,更新配置后自动更新调试框内容,无需保存。
10. 优化 - worker进程管理并将计算 Token 任务分配给 worker 进程。
11. 修复 - 工具调用时候name不能是数字开头随机数有概率数字开头
12. 修复 - 分享链接, query 全局变量会被缓存。

View File

@@ -50,8 +50,18 @@ export const replaceSensitiveText = (text: string) => {
return text;
};
/* Make sure the first letter is definitely lowercase */
export const getNanoid = (size = 12) => {
return customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', size)();
const firstChar = customAlphabet('abcdefghijklmnopqrstuvwxyz', 1)();
if (size === 1) return firstChar;
const randomsStr = customAlphabet(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
size - 1
)();
return `${firstChar}${randomsStr}`;
};
export const replaceRegChars = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

View File

@@ -155,6 +155,6 @@ export type ToolModuleResponseItemType = {
/* dispatch run time */
export type RuntimeUserPromptType = {
files?: UserChatItemValueItemType['file'][];
files: UserChatItemValueItemType['file'][];
text: string;
};

View File

@@ -1,7 +1,7 @@
import { DispatchNodeResponseType } from '../workflow/runtime/type';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../workflow/node/constant';
import { FlowNodeTypeEnum } from '../workflow/node/constant';
import { ChatItemValueTypeEnum, ChatRoleEnum } from './constants';
import { ChatHistoryItemResType, ChatItemType } from './type.d';
import { ChatHistoryItemResType, ChatItemType, UserChatItemValueItemType } from './type.d';
export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue = '新对话') => {
// @ts-ignore
@@ -77,3 +77,15 @@ export const filterPublicNodeResponseData = ({
return obj as ChatHistoryItemResType;
});
};
export const removeEmptyUserInput = (input: UserChatItemValueItemType[]) => {
return input.filter((item) => {
if (item.type === ChatItemValueTypeEnum.text && !item.text?.content?.trim()) {
return false;
}
if (item.type === ChatItemValueTypeEnum.file && !item.file?.url) {
return false;
}
return true;
});
};

View File

@@ -37,7 +37,6 @@ export enum NodeInputKeyEnum {
welcomeText = 'welcomeText',
switch = 'switch', // a trigger switch
history = 'history',
userChatInput = 'userChatInput',
answerText = 'text',
// system config
@@ -47,6 +46,10 @@ export enum NodeInputKeyEnum {
variables = 'variables',
scheduleTrigger = 'scheduleTrigger',
// entry
userChatInput = 'userChatInput',
inputFiles = 'inputFiles',
agents = 'agents', // cq agent key
// latest
@@ -101,7 +104,10 @@ export enum NodeInputKeyEnum {
// if else
condition = 'condition',
ifElseList = 'ifElseList'
ifElseList = 'ifElseList',
// variable update
updateList = 'updateList'
}
export enum NodeOutputKeyEnum {
@@ -135,15 +141,14 @@ export enum NodeOutputKeyEnum {
// plugin
pluginStart = 'pluginStart',
if = 'IF',
else = 'ELSE'
ifElseResult = 'ifElseResult'
}
export enum VariableInputEnum {
input = 'input',
textarea = 'textarea',
select = 'select',
external = 'external'
custom = 'custom'
}
export const variableMap = {
[VariableInputEnum.input]: {
@@ -161,10 +166,10 @@ export const variableMap = {
title: 'core.module.variable.select type',
desc: ''
},
[VariableInputEnum.external]: {
[VariableInputEnum.custom]: {
icon: 'core/app/variable/external',
title: 'core.module.variable.External type',
desc: '可以通过API接口或分享链接的Query传递变量。增加该类型变量的主要目的是用于变量提示。使用例子: 你可以通过分享链接Query中拼接Token来实现内部系统身份鉴权。'
title: 'core.module.variable.Custom type',
desc: '可以定义一个无需用户填写的全局变量。\n该变量的值可以来自于 API 接口,分享链接Query 或通过【变量更新】模块进行赋值。'
}
};

View File

@@ -112,7 +112,8 @@ export enum FlowNodeTypeEnum {
tools = 'tools',
stopTool = 'stopTool',
lafModule = 'lafModule',
ifElseNode = 'ifElseNode'
ifElseNode = 'ifElseNode',
variableUpdate = 'variableUpdate'
}
export const EDGE_TYPE = 'default';

View File

@@ -9,7 +9,8 @@ export enum SseResponseEventEnum {
toolCall = 'toolCall', // tool start
toolParams = 'toolParams', // tool params return
toolResponse = 'toolResponse', // tool response return
flowResponses = 'flowResponses' // sse response request
flowResponses = 'flowResponses', // sse response request
updateVariables = 'updateVariables'
}
export enum DispatchNodeResponseKeyEnum {

View File

@@ -75,7 +75,7 @@ export type DispatchNodeResponseType = {
pluginDetail?: ChatHistoryItemResType[];
// if-else
ifElseResult?: 'IF' | 'ELSE';
ifElseResult?: string;
// tool
toolCallTokens?: number;

View File

@@ -5,6 +5,8 @@ import { StoreNodeItemType } from '../type';
import { StoreEdgeItemType } from '../type/edge';
import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type';
import { VARIABLE_NODE_ID } from '../constants';
import { isReferenceValue } from '../utils';
import { ReferenceValueProps } from '../type/io';
export const initWorkflowEdgeStatus = (edges: StoreEdgeItemType[]): RuntimeEdgeItemType[] => {
return (
@@ -138,16 +140,11 @@ export const getReferenceVariableValue = ({
nodes,
variables
}: {
value: [string, string];
value: ReferenceValueProps;
nodes: RuntimeNodeItemType[];
variables: Record<string, any>;
}) => {
if (
!Array.isArray(value) ||
value.length !== 2 ||
typeof value[0] !== 'string' ||
typeof value[1] !== 'string'
) {
if (!isReferenceValue(value)) {
return value;
}
const sourceNodeId = value[0];

View File

@@ -18,10 +18,10 @@ import { PluginOutputModule } from './system/pluginOutput';
import { RunPluginModule } from './system/runPlugin';
import { AiQueryExtension } from './system/queryExtension';
import type { FlowNodeTemplateType, nodeTemplateListType } from '../type';
import { FlowNodeTemplateTypeEnum } from '../../workflow/constants';
import { lafModule } from './system/laf';
import { ifElseNode } from './system/ifElse/index';
import type { FlowNodeTemplateType } from '../type';
import { LafModule } from './system/laf';
import { IfElseNode } from './system/ifElse/index';
import { VariableUpdateNode } from './system/variableUpdate';
/* app flow module templates */
export const appSystemModuleTemplates: FlowNodeTemplateType[] = [
@@ -38,8 +38,9 @@ export const appSystemModuleTemplates: FlowNodeTemplateType[] = [
ContextExtractModule,
HttpModule468,
AiQueryExtension,
lafModule,
ifElseNode
LafModule,
IfElseNode,
VariableUpdateNode
];
/* plugin flow module templates */
export const pluginSystemModuleTemplates: FlowNodeTemplateType[] = [
@@ -56,8 +57,9 @@ export const pluginSystemModuleTemplates: FlowNodeTemplateType[] = [
ContextExtractModule,
HttpModule468,
AiQueryExtension,
lafModule,
ifElseNode
LafModule,
IfElseNode,
VariableUpdateNode
];
/* all module */
@@ -80,6 +82,7 @@ export const moduleTemplatesFlat: FlowNodeTemplateType[] = [
PluginOutputModule,
RunPluginModule,
AiQueryExtension,
lafModule,
ifElseNode
LafModule,
IfElseNode,
VariableUpdateNode
];

View File

@@ -9,9 +9,10 @@ export const Input_Template_History: FlowNodeInputItemType = {
renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.chatHistory,
label: 'core.module.input.label.chat history',
description: '最多携带多少轮对话记录',
required: true,
min: 0,
max: 30,
max: 50,
value: 6
};

View File

@@ -1,14 +1,9 @@
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '../../node/constant';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeTemplateType } from '../../type/index.d';
import {
WorkflowIOValueTypeEnum,
NodeInputKeyEnum,
FlowNodeTemplateTypeEnum,
NodeOutputKeyEnum
FlowNodeTemplateTypeEnum
} from '../../constants';
import { getHandleConfig } from '../utils';
@@ -26,7 +21,7 @@ export const AssignedAnswerModule: FlowNodeTemplateType = {
{
key: NodeInputKeyEnum.answerText,
renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.string,
valueType: WorkflowIOValueTypeEnum.any,
label: 'core.module.input.label.Response content',
description: 'core.module.input.description.Response content',
placeholder: 'core.module.input.description.Response content'

View File

@@ -1,6 +1,6 @@
import { FlowNodeTemplateTypeEnum, WorkflowIOValueTypeEnum } from '../../constants';
import { getHandleConfig } from '../utils';
import { FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../node/constant';
import { VariableItemType } from '../../../app/type';
import { FlowNodeTemplateType } from '../../type';
@@ -25,6 +25,7 @@ export const getGlobalVariableNode = ({
id: item.key,
key: item.key,
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.static,
label: item.label
}))
};

View File

@@ -20,6 +20,11 @@ export enum VariableConditionEnum {
lengthLessThan = 'lengthLessThan',
lengthLessThanOrEqualTo = 'lengthLessThanOrEqualTo'
}
export enum IfElseResultEnum {
IF = 'IF',
ELSE = 'ELSE',
ELSE_IF = 'ELSE IF'
}
export const stringConditionList = [
{ label: '为空', value: VariableConditionEnum.isEmpty },

View File

@@ -12,7 +12,7 @@ import {
import { FlowNodeTemplateType } from '../../../type';
import { getHandleConfig } from '../../utils';
export const ifElseNode: FlowNodeTemplateType = {
export const IfElseNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.ifElseNode,
templateType: FlowNodeTemplateTypeEnum.tools,
flowNodeType: FlowNodeTypeEnum.ifElseNode,
@@ -23,14 +23,6 @@ export const ifElseNode: FlowNodeTemplateType = {
intro: '根据一定的条件,执行不同的分支。',
showStatus: true,
inputs: [
{
key: NodeInputKeyEnum.condition,
valueType: WorkflowIOValueTypeEnum.string,
label: '',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
required: false,
value: 'AND' // AND, OR
},
{
key: NodeInputKeyEnum.ifElseList,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
@@ -38,27 +30,25 @@ export const ifElseNode: FlowNodeTemplateType = {
label: '',
value: [
{
variable: undefined,
condition: undefined,
value: undefined
condition: 'AND', // AND, OR
list: [
{
variable: undefined,
condition: undefined,
value: undefined
}
]
}
]
}
],
outputs: [
{
id: NodeOutputKeyEnum.if,
key: NodeOutputKeyEnum.if,
label: 'IF',
valueType: WorkflowIOValueTypeEnum.any,
type: FlowNodeOutputTypeEnum.source
},
{
id: NodeOutputKeyEnum.else,
key: NodeOutputKeyEnum.else,
label: 'ELSE',
valueType: WorkflowIOValueTypeEnum.any,
type: FlowNodeOutputTypeEnum.source
id: NodeOutputKeyEnum.ifElseResult,
key: NodeOutputKeyEnum.ifElseResult,
label: '判断结果',
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.static
}
]
};

View File

@@ -2,8 +2,12 @@ import { ReferenceValueProps } from 'core/workflow/type/io';
import { VariableConditionEnum } from './constant';
export type IfElseConditionType = 'AND' | 'OR';
export type IfElseListItemType = {
export type ConditionListItemType = {
variable?: ReferenceValueProps;
condition?: VariableConditionEnum;
value?: string;
};
export type IfElseListItemType = {
condition: IfElseConditionType;
list: ConditionListItemType[];
};

View File

@@ -14,7 +14,7 @@ import { Input_Template_DynamicInput } from '../input';
import { Output_Template_AddOutput } from '../output';
import { getHandleConfig } from '../utils';
export const lafModule: FlowNodeTemplateType = {
export const LafModule: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.lafModule,
templateType: FlowNodeTemplateTypeEnum.externalCall,
flowNodeType: FlowNodeTypeEnum.lafModule,

View File

@@ -12,7 +12,7 @@ export const PluginInputModule: FlowNodeTemplateType = {
unique: true,
forbidDelete: true,
avatar: '/imgs/workflow/input.png',
name: '定义插件输入',
name: '定义插件输入',
intro: '自定义配置外部输入,使用插件时,仅暴露自定义配置的输入',
showStatus: false,
inputs: [],

View File

@@ -12,7 +12,7 @@ export const PluginOutputModule: FlowNodeTemplateType = {
unique: true,
forbidDelete: true,
avatar: '/imgs/workflow/output.png',
name: '定义插件输出',
name: '定义插件输出',
intro: '自定义配置外部输出,使用插件时,仅暴露自定义配置的输出',
showStatus: false,
inputs: [],

View File

@@ -27,7 +27,7 @@ export const ToolModule: FlowNodeTemplateType = {
sourceHandle: getHandleConfig(true, true, false, true),
targetHandle: getHandleConfig(true, true, false, true),
avatar: '/imgs/workflow/tool.svg',
name: '工具调用实验',
name: '工具调用(实验)',
intro: '通过AI模型自动选择一个或多个功能块进行调用也可以对插件进行调用。',
showStatus: true,
inputs: [

View File

@@ -0,0 +1,42 @@
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant';
import { FlowNodeTemplateType } from '../../../type/index.d';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../../constants';
import { getHandleConfig } from '../../utils';
export const VariableUpdateNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.variableUpdate,
templateType: FlowNodeTemplateTypeEnum.tools,
flowNodeType: FlowNodeTypeEnum.variableUpdate,
sourceHandle: getHandleConfig(true, true, true, true),
targetHandle: getHandleConfig(true, true, true, true),
avatar: '/imgs/workflow/variable.png',
name: '变量更新',
intro: '可以更新指定节点的输出值和全局变量',
showStatus: true,
isTool: false,
inputs: [
{
key: NodeInputKeyEnum.updateList,
valueType: WorkflowIOValueTypeEnum.any,
label: '',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
editField: {
key: true,
valueType: true
},
value: [
{
variable: ['', ''],
value: ['', ''],
valueType: WorkflowIOValueTypeEnum.string,
renderType: FlowNodeInputTypeEnum.input
}
]
}
],
outputs: []
};

View File

@@ -0,0 +1,10 @@
import { FlowNodeInputTypeEnum } from '../../../node/constant';
import { ReferenceValueProps } from '../../..//type/io';
import { WorkflowIOValueTypeEnum } from '../../../constants';
export type TUpdateListItem = {
variable?: ReferenceValueProps;
value: ReferenceValueProps;
valueType?: WorkflowIOValueTypeEnum;
renderType: FlowNodeInputTypeEnum.input | FlowNodeInputTypeEnum.reference;
};

View File

@@ -40,7 +40,7 @@ export type FlowNodeCommonType = {
};
export type FlowNodeTemplateType = FlowNodeCommonType & {
id: string; // module id, unique
id: string; // node id, unique
templateType: `${FlowNodeTemplateTypeEnum}`;
// show handle
@@ -132,11 +132,12 @@ export type ChatDispatchProps = {
chatId?: string;
responseChatItemId?: string;
histories: ChatItemType[];
variables: Record<string, any>;
inputFiles?: UserChatItemValueItemType['file'][];
variables: Record<string, any>; // global variable
query: UserChatItemValueItemType[]; // trigger query
stream: boolean;
detail: boolean; // response detail
maxRunTimes: number;
isToolCall?: boolean;
};
export type ModuleDispatchProps<T> = ChatDispatchProps & {

View File

@@ -15,6 +15,7 @@ import type {
} from '../app/type';
import { EditorVariablePickerType } from '../../../web/components/common/Textarea/PromptEditor/type';
import { defaultWhisperConfig } from '../app/constants';
import { IfElseResultEnum } from './template/system/ifElse/constant';
export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => {
return `${nodeId}-${type}-${key}`;
@@ -132,3 +133,11 @@ export const formatEditorVariablePickerIcon = (
icon: item.type ? variableMap[item.type]?.icon : variableMap['input'].icon
}));
};
export const isReferenceValue = (value: any): boolean => {
return Array.isArray(value) && value.length === 2 && typeof value[0] === 'string';
};
export const getElseIFLabel = (i: number) => {
return i === 0 ? IfElseResultEnum.IF : `${IfElseResultEnum.ELSE_IF} ${i}`;
};

View File

@@ -1,3 +1,4 @@
import { addLog } from '../../../common/system/log';
import { POST } from '../../../common/api/serverRequest';
type PostReRankResponse = {
@@ -38,7 +39,7 @@ export function reRankRecall({
}
)
.then((data) => {
console.log('rerank time:', Date.now() - start);
addLog.info('ReRank finish:', { time: Date.now() - start });
return data?.results?.map((item) => ({
id: documents[item.index].id,
@@ -46,7 +47,7 @@ export function reRankRecall({
}));
})
.catch((err) => {
console.log('rerank error:', err);
addLog.error('rerank error', err);
return [];
});

View File

@@ -1,5 +1,6 @@
import { PluginTemplateType } from '@fastgpt/global/core/plugin/type.d';
declare global {
var communityPluginsV1: PluginTemplateType[];
var communityPlugins: PluginTemplateType[];
}

View File

@@ -161,6 +161,7 @@ export const runToolWithFunctionCall = async (
const toolRunResponse = await dispatchWorkFlow({
...props,
isToolCall: true,
runtimeNodes: runtimeNodes.map((item) =>
item.nodeId === toolNode.nodeId
? {

View File

@@ -185,6 +185,7 @@ export const runToolWithPromptCall = async (
const moduleRunResponse = await dispatchWorkFlow({
...props,
isToolCall: true,
runtimeNodes: runtimeNodes.map((item) =>
item.nodeId === toolNode.nodeId
? {

View File

@@ -182,6 +182,7 @@ export const runToolWithToolChoice = async (
const toolRunResponse = await dispatchWorkFlow({
...props,
isToolCall: true,
runtimeNodes: runtimeNodes.map((item) =>
item.nodeId === toolNode.nodeId
? {

View File

@@ -25,6 +25,7 @@ import {
} from '../../../../common/string/tiktoken/index';
import {
chats2GPTMessages,
chatValue2RuntimePrompt,
getSystemPrompt,
GPTMessages2Chats,
runtimePrompt2ChatsValue
@@ -66,7 +67,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
user,
histories,
node: { name },
inputFiles = [],
query,
params: {
model,
temperature = 0,
@@ -80,6 +81,8 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
quotePrompt
}
} = props;
const { files: inputFiles } = chatValue2RuntimePrompt(query);
if (!userChatInput && inputFiles.length === 0) {
return Promise.reject('Question is empty');
}

View File

@@ -42,7 +42,8 @@ import { dispatchLafRequest } from './tools/runLaf';
import { dispatchIfElse } from './tools/runIfElse';
import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
import { dispatchSystemConfig } from './init/systemConfiig';
import { dispatchSystemConfig } from './init/systemConfig';
import { dispatchUpdateVariable } from './tools/runUpdateVar';
const callbackMap: Record<`${FlowNodeTypeEnum}`, Function> = {
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
@@ -62,6 +63,7 @@ const callbackMap: Record<`${FlowNodeTypeEnum}`, Function> = {
[FlowNodeTypeEnum.stopTool]: dispatchStopToolCall,
[FlowNodeTypeEnum.lafModule]: dispatchLafRequest,
[FlowNodeTypeEnum.ifElseNode]: dispatchIfElse,
[FlowNodeTypeEnum.variableUpdate]: dispatchUpdateVariable,
// none
[FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig,
@@ -69,21 +71,25 @@ const callbackMap: Record<`${FlowNodeTypeEnum}`, Function> = {
[FlowNodeTypeEnum.globalVariable]: () => Promise.resolve()
};
/* running */
export async function dispatchWorkFlow({
res,
runtimeNodes = [],
runtimeEdges = [],
histories = [],
variables = {},
user,
stream = false,
detail = false,
...props
}: ChatDispatchProps & {
type Props = ChatDispatchProps & {
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
}): Promise<DispatchFlowResponse> {
};
/* running */
export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowResponse> {
let {
res,
runtimeNodes = [],
runtimeEdges = [],
histories = [],
variables = {},
user,
stream = false,
detail = false,
...props
} = data;
// set sse response headers
if (stream && res) {
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
@@ -93,7 +99,7 @@ export async function dispatchWorkFlow({
}
variables = {
...getSystemVariable({ timezone: user.timezone }),
...getSystemVariable(data),
...variables
};
@@ -283,7 +289,8 @@ export async function dispatchWorkFlow({
node,
runtimeNodes,
runtimeEdges,
params
params,
mode: 'test'
};
// run module
@@ -362,7 +369,8 @@ export async function dispatchWorkFlow({
},
[DispatchNodeResponseKeyEnum.assistantResponses]:
mergeAssistantResponseAnswerText(chatAssistantResponse),
[DispatchNodeResponseKeyEnum.toolResponses]: toolRunResponse
[DispatchNodeResponseKeyEnum.toolResponses]: toolRunResponse,
newVariables: removeSystemVariable(variables)
};
}
@@ -384,12 +392,34 @@ export function responseStatus({
}
/* get system variable */
export function getSystemVariable({ timezone }: { timezone: string }) {
export function getSystemVariable({
user,
appId,
chatId,
responseChatItemId,
histories = []
}: Props) {
return {
cTime: getSystemTime(timezone)
appId,
chatId,
responseChatItemId,
histories,
cTime: getSystemTime(user.timezone)
};
}
/* remove system variable */
const removeSystemVariable = (variables: Record<string, any>) => {
const copyVariables = { ...variables };
delete copyVariables.appId;
delete copyVariables.chatId;
delete copyVariables.responseChatItemId;
delete copyVariables.histories;
delete copyVariables.cTime;
return copyVariables;
};
/* Merge consecutive text messages into one */
export const mergeAssistantResponseAnswerText = (response: AIChatItemValueItemType[]) => {
const result: AIChatItemValueItemType[] = [];

View File

@@ -0,0 +1,4 @@
export const dispatchSystemConfig = (props: Record<string, any>) => {
const { variables } = props;
return variables;
};

View File

@@ -1,10 +0,0 @@
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type/index.d';
export type UserChatInputProps = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string;
}>;
export const dispatchSystemConfig = (props: Record<string, any>) => {
const { variables } = props as UserChatInputProps;
return variables;
};

View File

@@ -1,15 +1,22 @@
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type/index.d';
export type UserChatInputProps = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.inputFiles]: UserChatItemValueItemType['file'][];
}>;
export const dispatchWorkflowStart = (props: Record<string, any>) => {
const {
variables: { userChatInput },
params: { userChatInput: query }
query,
params: { userChatInput }
} = props as UserChatInputProps;
const { text, files } = chatValue2RuntimePrompt(query);
return {
userChatInput: query || userChatInput
[NodeInputKeyEnum.userChatInput]: text || userChatInput,
[NodeInputKeyEnum.inputFiles]: files
};
};

View File

@@ -1,7 +1,7 @@
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type/index.d';
import { dispatchWorkFlow } from '../index';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getPluginRuntimeById } from '../../../plugin/controller';
import { authPluginCanUse } from '../../../../support/permission/auth/plugin';

View File

@@ -49,6 +49,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
variables,
node: { outputs },
histories,
isToolCall,
params: {
system_httpMethod: httpMethod = 'POST',
system_httpReqUrl: httpReqUrl,
@@ -156,17 +157,21 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
};
} catch (error) {
addLog.error('Http request error', error);
return {
[NodeOutputKeyEnum.failed]: true,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0,
params: Object.keys(params).length > 0 ? params : undefined,
body: Object.keys(requestBody).length > 0 ? requestBody : undefined,
headers: Object.keys(headers).length > 0 ? headers : undefined,
httpResult: { error: formatHttpError(error) }
},
[NodeOutputKeyEnum.httpRawResponse]: getErrText(error)
};
if (isToolCall) {
return {
[NodeOutputKeyEnum.failed]: true,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0,
params: Object.keys(params).length > 0 ? params : undefined,
body: Object.keys(requestBody).length > 0 ? requestBody : undefined,
headers: Object.keys(headers).length > 0 ? headers : undefined,
httpResult: { error: formatHttpError(error) }
},
[NodeOutputKeyEnum.httpRawResponse]: getErrText(error)
};
}
return Promise.reject(error);
}
};

View File

@@ -35,7 +35,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
stream,
detail,
histories,
inputFiles,
query,
params: { userChatInput, history, app }
} = props;
let start = Date.now();
@@ -71,7 +71,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
runtimeNodes: storeNodes2RuntimeNodes(appData.modules, getDefaultEntryNodeIds(appData.modules)),
runtimeEdges: initWorkflowEdgeStatus(appData.edges),
histories: chatHistories,
inputFiles,
query,
variables: {
...props.variables,
userChatInput
@@ -81,10 +81,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
const completeMessages = chatHistories.concat([
{
obj: ChatRoleEnum.Human,
value: runtimePrompt2ChatsValue({
files: inputFiles,
text: userChatInput
})
value: query
},
{
obj: ChatRoleEnum.AI,

View File

@@ -1,19 +1,26 @@
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { VariableConditionEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import {
IfElseResultEnum,
VariableConditionEnum
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import {
ConditionListItemType,
IfElseConditionType,
IfElseListItemType
} from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { getElseIFLabel, getHandleId } from '@fastgpt/global/core/workflow/utils';
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.condition]: IfElseConditionType;
[NodeInputKeyEnum.ifElseList]: IfElseListItemType[];
}>;
type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.ifElseResult]: string;
}>;
function checkCondition(condition: VariableConditionEnum, variableValue: any, value: string) {
const operations = {
@@ -41,15 +48,13 @@ function checkCondition(condition: VariableConditionEnum, variableValue: any, va
return (operations[condition] || (() => false))();
}
export const dispatchIfElse = async (props: Props): Promise<DispatchNodeResultType<{}>> => {
const {
params,
runtimeNodes,
variables,
node: { nodeId }
} = props;
const { condition, ifElseList } = params;
const listResult = ifElseList.map((item) => {
function getResult(
condition: IfElseConditionType,
list: ConditionListItemType[],
variables: any,
runtimeNodes: any[]
) {
const listResult = list.map((item) => {
const { variable, condition: variableCondition, value } = item;
const variableValue = getReferenceVariableValue({
@@ -61,15 +66,41 @@ export const dispatchIfElse = async (props: Props): Promise<DispatchNodeResultTy
return checkCondition(variableCondition as VariableConditionEnum, variableValue, value || '');
});
const result = condition === 'AND' ? listResult.every(Boolean) : listResult.some(Boolean);
return condition === 'AND' ? listResult.every(Boolean) : listResult.some(Boolean);
}
export const dispatchIfElse = async (props: Props): Promise<Response> => {
const {
params,
runtimeNodes,
variables,
node: { nodeId }
} = props;
const { ifElseList } = params;
let res = IfElseResultEnum.ELSE as string;
for (let i = 0; i < ifElseList.length; i++) {
const item = ifElseList[i];
const result = getResult(item.condition, item.list, variables, runtimeNodes);
if (result) {
res = getElseIFLabel(i);
break;
}
}
const resArray = Array.from({ length: ifElseList.length + 1 }, (_, index) => {
const label = index < ifElseList.length ? getElseIFLabel(index) : IfElseResultEnum.ELSE;
return getHandleId(nodeId, 'source', label);
});
return {
[NodeOutputKeyEnum.ifElseResult]: res,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0,
ifElseResult: result ? 'IF' : 'ELSE'
ifElseResult: res
},
[DispatchNodeResponseKeyEnum.skipHandleId]: result
? [getHandleId(nodeId, 'source', 'ELSE')]
: [getHandleId(nodeId, 'source', 'IF')]
[DispatchNodeResponseKeyEnum.skipHandleId]: resArray.filter(
(item) => item !== getHandleId(nodeId, 'source', res)
)
};
};

View File

@@ -0,0 +1,59 @@
import { NodeInputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type';
import { valueTypeFormat } from '../utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.updateList]: TUpdateListItem[];
}>;
export const dispatchUpdateVariable = async (
props: Props
): Promise<DispatchNodeResultType<any>> => {
const { params, variables, runtimeNodes } = props;
const { updateList } = params;
updateList.forEach((item) => {
const varNodeId = item.variable?.[0];
const varKey = item.variable?.[1];
if (!varNodeId || !varKey) {
return;
}
const value = (() => {
if (!item.value?.[0]) {
return valueTypeFormat(item.value?.[1], item.valueType);
} else {
return getReferenceVariableValue({
value: item.value,
variables,
nodes: runtimeNodes
});
}
})();
if (varNodeId === VARIABLE_NODE_ID) {
// update global variable
variables[varKey] = value;
} else {
runtimeNodes
.find((node) => node.nodeId === varNodeId)
?.outputs?.find((output) => {
if (output.id === varKey) {
output.value = value;
return true;
}
});
}
});
return {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0
}
};
};

View File

@@ -19,4 +19,5 @@ export type DispatchFlowResponse = {
};
[DispatchNodeResponseKeyEnum.toolResponses]: ToolRunResponseItemType;
[DispatchNodeResponseKeyEnum.assistantResponses]: AIChatItemValueItemType[];
newVariables: Record<string, string>;
};

View File

@@ -47,7 +47,7 @@ export const filterToolNodeIdByEdges = ({
export const getHistories = (history?: ChatItemType[] | number, histories: ChatItemType[] = []) => {
if (!history) return [];
if (typeof history === 'number') return histories.slice(-history);
if (typeof history === 'number') return histories.slice(-(history * 2));
if (Array.isArray(history)) return history;
return [];

View File

@@ -279,7 +279,7 @@ export async function dispatchWorkFlowV1({
)?.targets?.length;
return moduleOutput(module, {
[NodeOutputKeyEnum.finish]: true,
finish: true,
[NodeOutputKeyEnum.userChatInput]: hasUserChatInputTarget
? params[NodeOutputKeyEnum.userChatInput]
: undefined,
@@ -295,7 +295,7 @@ export async function dispatchWorkFlowV1({
modules.forEach((item) => {
item.isEntry = false;
});
// console.log(JSON.stringify(runningModules, null, 2));
initModules.map((module) =>
moduleInput(module, {
...startParams,

View File

@@ -1,13 +1,20 @@
// @ts-nocheck
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type';
import { dispatchWorkFlowV1 } from '../index';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum
} from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getPluginRuntimeById } from '../../../plugin/controller';
import { splitCombinePluginId } from '../../../plugin/controller';
import { authPluginCanUse } from '../../../../support/permission/auth/plugin';
import { setEntryEntries, DYNAMIC_INPUT_KEY } from '../utils';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { PluginRuntimeType, PluginTemplateType } from '@fastgpt/global/core/plugin/type';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { MongoPlugin } from '../../../plugin/schema';
type RunPluginProps = ModuleDispatchProps<{
[NodeInputKeyEnum.pluginId]: string;
@@ -15,6 +22,45 @@ type RunPluginProps = ModuleDispatchProps<{
}>;
type RunPluginResponse = DispatchNodeResultType<{}>;
const getPluginTemplateById = async (id: string): Promise<PluginTemplateType> => {
const { source, pluginId } = await splitCombinePluginId(id);
if (source === PluginSourceEnum.community) {
const item = global.communityPluginsV1?.find((plugin) => plugin.id === pluginId);
if (!item) return Promise.reject('plugin not found');
return item;
}
if (source === PluginSourceEnum.personal) {
const item = await MongoPlugin.findById(id).lean();
if (!item) return Promise.reject('plugin not found');
return {
id: String(item._id),
teamId: String(item.teamId),
name: item.name,
avatar: item.avatar,
intro: item.intro,
showStatus: true,
source: PluginSourceEnum.personal,
modules: item.modules,
templateType: FlowNodeTemplateTypeEnum.personalPlugin
};
}
return Promise.reject('plugin not found');
};
const getPluginRuntimeById = async (id: string): Promise<PluginRuntimeType> => {
const plugin = await getPluginTemplateById(id);
return {
teamId: plugin.teamId,
name: plugin.name,
avatar: plugin.avatar,
showStatus: plugin.showStatus,
modules: plugin.modules
};
};
export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPluginResponse> => {
const {
mode,
@@ -31,7 +77,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
const plugin = await getPluginRuntimeById(pluginId);
// concat dynamic inputs
const inputModule = plugin.nodes.find((item) => item.flowType === FlowNodeTypeEnum.pluginInput);
const inputModule = plugin.modules.find((item) => item.flowType === FlowNodeTypeEnum.pluginInput);
if (!inputModule) return Promise.reject('Plugin error, It has no set input.');
const hasDynamicInput = inputModule.inputs.find((input) => input.key === DYNAMIC_INPUT_KEY);
@@ -56,7 +102,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
const { flowResponses, flowUsages, assistantResponses } = await dispatchWorkFlowV1({
...props,
modules: setEntryEntries(plugin.nodes).map((module) => ({
modules: setEntryEntries(plugin.modules).map((module) => ({
...module,
showStatus: false
})),

View File

@@ -32,6 +32,6 @@ export const dispatchAnswer = (props: Record<string, any>): AnswerResponse => {
}
return {
[NodeOutputKeyEnum.answerText]: formatText
[NodeOutputKeyEnum.answerText]: `\n${formatText}`
};
};

View File

@@ -12,4 +12,5 @@ export type DispatchFlowResponse = {
flowUsages: ChatNodeUsageType[];
[DispatchNodeResponseKeyEnum.toolResponses]: ToolRunResponseItemType;
[DispatchNodeResponseKeyEnum.assistantResponses]: AIChatItemValueItemType[];
newVariables: Record<string, string>;
};

View File

@@ -22,7 +22,10 @@ export async function getUserDetail({
}): Promise<UserType> {
const tmb = await (async () => {
if (tmbId) {
return getTmbInfoByTmbId({ tmbId });
try {
const result = await getTmbInfoByTmbId({ tmbId });
return result;
} catch (error) {}
}
if (userId) {
return getUserDefaultTeam({ userId });

View File

@@ -0,0 +1,14 @@
import { DragHandleIcon } from '@chakra-ui/icons';
import { Box } from '@chakra-ui/react';
import React from 'react';
import { DraggableProvided } from 'react-beautiful-dnd';
const DragIcon = ({ provided }: { provided: DraggableProvided }) => {
return (
<Box {...provided.dragHandleProps}>
<DragHandleIcon color={'myGray.500'} _hover={{ color: 'primary.600' }} />
</Box>
);
};
export default DragIcon;

View File

@@ -0,0 +1,61 @@
import { Box } from '@chakra-ui/react';
import React, { useState } from 'react';
import {
DragDropContext,
DroppableProps,
Droppable,
DraggableChildrenFn,
DragStart,
DropResult
} from 'react-beautiful-dnd';
type Props<T = any> = {
onDragEndCb: (result: T[]) => void;
renderClone?: DraggableChildrenFn;
children: DroppableProps['children'];
dataList: T[];
};
function DndDrag<T>({ children, renderClone, onDragEndCb, dataList }: Props<T>) {
const [draggingItemHeight, setDraggingItemHeight] = useState(0);
const onDragStart = (start: DragStart) => {
const draggingNode = document.querySelector(`[data-rbd-draggable-id="${start.draggableId}"]`);
setDraggingItemHeight(draggingNode?.getBoundingClientRect().height || 0);
};
const onDragEnd = (result: DropResult) => {
if (!result.destination) {
return;
}
setDraggingItemHeight(0);
const startIndex = result.source.index;
const endIndex = result.destination.index;
const list = Array.from(dataList);
const [removed] = list.splice(startIndex, 1);
list.splice(endIndex, 0, removed);
onDragEndCb(list);
};
return (
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<Droppable droppableId="droppable" renderClone={renderClone}>
{(provided, snapshot) => {
return (
<Box {...provided.droppableProps} ref={provided.innerRef}>
{children(provided, snapshot)}
{snapshot.isDraggingOver && <Box height={draggingItemHeight} />}
</Box>
);
}}
</Droppable>
</DragDropContext>
);
}
export default DndDrag;
export * from 'react-beautiful-dnd';

View File

@@ -8,13 +8,15 @@ export const useBeforeunload = (props?: { callback?: () => any; tip?: string })
useEffect(() => {
const listen =
process.env.NODE_ENV !== 'production'
process.env.NODE_ENV === 'production'
? (e: any) => {
e.preventDefault();
e.returnValue = tip;
callback?.();
}
: () => {};
: () => {
callback?.();
};
window.addEventListener('beforeunload', listen);
return () => {

View File

@@ -92,9 +92,9 @@ export const useEditTextarea = ({
closeBtnText?: string;
}) => (
<MyModal isOpen={isOpen} onClose={onClose} iconSrc={iconSrc} title={title} maxW={'500px'}>
<ModalBody>
<ModalBody pt={tip ? '3 !important' : '5 !important'}>
{!!tip && (
<Box mb={2} color={'myGray.500'} fontSize={'sm'}>
<Box mb={3} color={'myGray.500'} fontSize={'sm'}>
{tip}
</Box>
)}

View File

@@ -31,12 +31,14 @@
"react": "18.2.0",
"react-day-picker": "^8.7.1",
"react-dom": "18.2.0",
"react-i18next": "13.5.0"
"react-i18next": "13.5.0",
"react-beautiful-dnd": "^13.1.1"
},
"devDependencies": {
"@types/lodash": "^4.14.191",
"@types/papaparse": "^5.3.7",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0"
"@types/react-dom": "18.2.0",
"@types/react-beautiful-dnd": "^13.1.8"
}
}

120
pnpm-lock.yaml generated
View File

@@ -292,6 +292,9 @@ importers:
react:
specifier: 18.2.0
version: 18.2.0
react-beautiful-dnd:
specifier: ^13.1.1
version: 13.1.1(react-dom@18.2.0)(react@18.2.0)
react-day-picker:
specifier: ^8.7.1
version: 8.7.1(date-fns@2.30.0)(react@18.2.0)
@@ -311,6 +314,9 @@ importers:
'@types/react':
specifier: 18.2.0
version: 18.2.0
'@types/react-beautiful-dnd':
specifier: ^13.1.8
version: 13.1.8
'@types/react-dom':
specifier: 18.2.0
version: 18.2.0
@@ -3065,6 +3071,7 @@ packages:
/@emotion/memoize@0.7.4:
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
requiresBuild: true
dev: false
optional: true
@@ -3682,6 +3689,7 @@ packages:
/@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13):
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
requiresBuild: true
dependencies:
detect-libc: 2.0.3
https-proxy-agent: 5.0.1
@@ -4741,12 +4749,27 @@ packages:
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
dev: true
/@types/react-beautiful-dnd@13.1.8:
resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==}
dependencies:
'@types/react': 18.2.0
dev: true
/@types/react-dom@18.2.0:
resolution: {integrity: sha512-8yQrvS6sMpSwIovhPOwfyNf2Wz6v/B62LFSVYQ85+Rq3tLsBIG7rP5geMxaijTUxSkrO6RzN/IRuIAADYQsleA==}
dependencies:
'@types/react': 18.2.0
dev: true
/@types/react-redux@7.1.33:
resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==}
dependencies:
'@types/hoist-non-react-statics': 3.3.5
'@types/react': 18.2.0
hoist-non-react-statics: 3.3.2
redux: 4.2.1
dev: false
/@types/react-syntax-highlighter@15.5.6:
resolution: {integrity: sha512-i7wFuLbIAFlabTeD2I1cLjEOrG/xdMa/rpx2zwzAoGHuXJDhSqp9BSfDlMHSh9JSuNfxHk9eEmMX6D55GiyjGg==}
dependencies:
@@ -5005,6 +5028,7 @@ packages:
/abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
requiresBuild: true
dev: false
optional: true
@@ -5047,6 +5071,7 @@ packages:
/agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
requiresBuild: true
dependencies:
debug: 4.3.4
transitivePeerDependencies:
@@ -5170,12 +5195,14 @@ packages:
/aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
requiresBuild: true
dev: false
optional: true
/are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'}
requiresBuild: true
dependencies:
delegates: 1.0.0
readable-stream: 3.6.2
@@ -5791,6 +5818,7 @@ packages:
/chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
requiresBuild: true
dev: false
optional: true
@@ -5903,6 +5931,7 @@ packages:
/color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
requiresBuild: true
dev: false
optional: true
@@ -5975,6 +6004,7 @@ packages:
/console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
requiresBuild: true
dev: false
optional: true
@@ -6540,6 +6570,7 @@ packages:
/decompress-response@4.2.1:
resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==}
engines: {node: '>=8'}
requiresBuild: true
dependencies:
mimic-response: 2.1.0
dev: false
@@ -6644,6 +6675,7 @@ packages:
/delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
requiresBuild: true
dev: false
optional: true
@@ -6671,6 +6703,7 @@ packages:
/detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
requiresBuild: true
dev: false
optional: true
@@ -7877,6 +7910,7 @@ packages:
/fs-minipass@2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
dev: false
@@ -7912,6 +7946,7 @@ packages:
/gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
requiresBuild: true
dependencies:
aproba: 2.0.0
color-support: 1.1.3
@@ -8089,6 +8124,7 @@ packages:
/has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
requiresBuild: true
dev: false
optional: true
@@ -8246,6 +8282,7 @@ packages:
/https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
requiresBuild: true
dependencies:
agent-base: 6.0.2
debug: 4.3.4
@@ -9088,6 +9125,7 @@ packages:
/make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
requiresBuild: true
dependencies:
semver: 6.3.1
dev: false
@@ -9289,8 +9327,13 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
dev: false
/memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
requiresBuild: true
dev: false
optional: true
@@ -9647,6 +9690,7 @@ packages:
/mimic-response@2.1.0:
resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==}
engines: {node: '>=8'}
requiresBuild: true
dev: false
optional: true
@@ -9669,6 +9713,7 @@ packages:
/minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
requiresBuild: true
dependencies:
yallist: 4.0.0
dev: false
@@ -9677,12 +9722,14 @@ packages:
/minipass@5.0.0:
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
engines: {node: '>=8'}
requiresBuild: true
dev: false
optional: true
/minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
requiresBuild: true
dependencies:
minipass: 3.3.6
yallist: 4.0.0
@@ -9700,6 +9747,7 @@ packages:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
requiresBuild: true
dev: false
optional: true
@@ -9981,6 +10029,7 @@ packages:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
hasBin: true
requiresBuild: true
dependencies:
abbrev: 1.1.1
dev: false
@@ -9999,6 +10048,7 @@ packages:
/npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
requiresBuild: true
dependencies:
are-we-there-yet: 2.0.0
console-control-strings: 1.1.0
@@ -10309,6 +10359,7 @@ packages:
/path2d@0.1.1:
resolution: {integrity: sha512-/+S03c8AGsDYKKBtRDqieTJv2GlkMb0bWjnqOgtF6MkjdUQ9a8ARAtxWf9NgKLGm2+WQr6+/tqJdU8HNGsIDoA==}
engines: {node: '>=6'}
requiresBuild: true
dev: false
optional: true
@@ -10587,6 +10638,10 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
/raf-schd@4.0.3:
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
dev: false
/randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
dependencies:
@@ -10614,6 +10669,25 @@ packages:
unpipe: 1.0.0
dev: false
/react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
peerDependencies:
react: ^16.8.5 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.24.1
css-box-model: 1.2.1
memoize-one: 5.2.1
raf-schd: 4.0.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0)
redux: 4.2.1
use-memo-one: 1.1.3(react@18.2.0)
transitivePeerDependencies:
- react-native
dev: false
/react-clientside-effect@1.2.6(react@18.2.0):
resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==}
peerDependencies:
@@ -10706,6 +10780,10 @@ packages:
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
/react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: false
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: false
@@ -10737,6 +10815,28 @@ packages:
- supports-color
dev: false
/react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
peerDependencies:
react: ^16.8.3 || ^17 || ^18
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.24.1
'@types/react-redux': 7.1.33
hoist-non-react-statics: 3.3.2
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 17.0.2
dev: false
/react-remove-scroll-bar@2.3.6(@types/react@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
@@ -10858,6 +10958,12 @@ packages:
dependencies:
picomatch: 2.3.1
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
'@babel/runtime': 7.24.1
dev: false
/reflect.getprototypeof@1.0.6:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'}
@@ -11262,6 +11368,7 @@ packages:
/set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
requiresBuild: true
dev: false
optional: true
@@ -11331,11 +11438,13 @@ packages:
/simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
requiresBuild: true
dev: false
optional: true
/simple-get@3.1.1:
resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==}
requiresBuild: true
dependencies:
decompress-response: 4.2.1
once: 1.4.0
@@ -11417,6 +11526,7 @@ packages:
/sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
requiresBuild: true
dependencies:
memory-pager: 1.5.0
dev: false
@@ -11685,6 +11795,7 @@ packages:
/tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
requiresBuild: true
dependencies:
chownr: 2.0.0
fs-minipass: 2.1.0
@@ -12182,6 +12293,14 @@ packages:
scheduler: 0.23.0
dev: false
/use-memo-one@1.1.3(react@18.2.0):
resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/use-sidecar@1.1.2(@types/react@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
@@ -12539,6 +12658,7 @@ packages:
/wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
requiresBuild: true
dependencies:
string-width: 4.2.3
dev: false

View File

@@ -7,336 +7,205 @@
"showStatus": false,
"isTool": false,
"weight": 0,
"modules": [
"nodes": [
{
"moduleId": "w90mfp",
"name": "定义插件输入",
"flowType": "pluginInput",
"nodeId": "lmpb9v2lo2lk",
"name": "定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 515.1887815471657,
"y": -169.04905809653783
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [
{
"key": "defaultFeedback",
"valueType": "string",
"label": "默认反馈内容",
"type": "textarea",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "",
"edit": true,
"canEdit": true,
"value": "",
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
"key": true
},
"connected": true
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
},
{
"key": "customFeedback",
"key": "反馈内容",
"valueType": "string",
"label": "自定义反馈内容",
"type": "target",
"required": false,
"label": "反馈内容",
"renderTypeList": ["textarea"],
"description": "",
"edit": true,
"canEdit": true,
"value": "",
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
"key": true
},
"connected": true
"maxLength": "",
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
}
],
"outputs": [
{
"key": "defaultFeedback",
"id": "ILc8GS7iU53M",
"key": "反馈内容",
"valueType": "string",
"label": "默认反馈内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "defaultFeedback"
}
]
"label": "反馈内容",
"type": "static"
},
{
"key": "customFeedback",
"valueType": "string",
"label": "自定义反馈内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "customFeedback"
}
]
"id": "2LCxDnOSculb",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"type": "static"
}
]
},
{
"moduleId": "49de3g",
"name": "HTTP模块",
"flowType": "httpRequest468",
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"inputs": [],
"outputs": []
},
{
"nodeId": "CRT7oIEU8v2P",
"name": "HTTP 请求",
"intro": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)",
"avatar": "/imgs/workflow/http.png",
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1086.8929621216014,
"y": -451.7550009773506
"x": 1070.8458389994719,
"y": -415.09022555407836
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "system_httpMethod",
"type": "custom",
"valueType": "string",
"label": "",
"value": "POST",
"list": [
{
"label": "GET",
"value": "GET"
},
{
"label": "POST",
"value": "POST"
}
],
"required": true,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpReqUrl",
"type": "hidden",
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "/api/plugins/customFeedback",
"connected": false
},
{
"key": "system_httpHeader",
"type": "custom",
"valueType": "any",
"value": "",
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpParams",
"type": "hidden",
"valueType": "any",
"value": [],
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpJsonBody",
"type": "hidden",
"valueType": "any",
"value": "{\r\n \"appId\": \"{{appId}}\",\r\n \"chatId\": \"{{chatId}}\",\r\n \"responseChatItemId\": \"{{responseChatItemId}}\",\r\n \"defaultFeedback\": \"{{defaultFeedback}}\",\r\n \"customFeedback\": \"{{customFeedback}}\"\r\n}",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.workflow.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": true,
"hideInApp": true,
"connected": false
},
{
"valueType": "string",
"label": "defaultFeedback",
"type": "target",
"required": true,
"description": "",
"edit": true,
"description": "core.module.input.description.HTTP Dynamic Input",
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
"valueType": true
},
"connected": true,
"key": "defaultFeedback"
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"]
},
{
"key": "customFeedback",
"valueType": "string",
"label": "customFeedback",
"type": "target",
"required": true,
"renderTypeList": ["reference"],
"description": "",
"edit": true,
"canEdit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
"valueType": true
},
"connected": true
"value": ["lmpb9v2lo2lk", "ILc8GS7iU53M"]
},
{
"key": "system_addInputParam",
"type": "addInputParam",
"valueType": "any",
"key": "system_httpMethod",
"renderTypeList": ["custom"],
"valueType": "string",
"label": "",
"value": "POST",
"required": true
},
{
"key": "system_httpReqUrl",
"renderTypeList": ["hidden"],
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
"value": "/api/plugins/customFeedback/v2"
},
{
"key": "system_httpHeader",
"renderTypeList": ["custom"],
"valueType": "any",
"value": [],
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false
},
{
"key": "system_httpParams",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
"required": false
},
{
"key": "system_httpJsonBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": "{\r\n \"customFeedback\":\"{{customFeedback}}\",\r\n \"system_addInputParam\": {{system_addInputParam}}\r\n}",
"label": "",
"required": false
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
},
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "addOutputParam",
"valueType": "any",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"targets": [],
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"outputType": "source",
"valueType": "string"
"valueType": true
}
},
{
"type": "source",
"valueType": "string",
"label": "response",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "s15f3v",
"key": "text"
}
],
"key": "response"
}
]
},
{
"moduleId": "s15f3v",
"name": "指定回复",
"flowType": "answerNode",
"position": {
"x": 1705.6337348182756,
"y": -37.53826066726282
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "text",
"type": "textarea",
"valueType": "any",
"label": "core.module.input.label.Response content",
"description": "core.module.input.description.Response content",
"placeholder": "core.module.input.description.Response content",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": true
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
"type": "static"
}
]
}
],
"edges": [
{
"source": "lmpb9v2lo2lk",
"target": "CRT7oIEU8v2P",
"sourceHandle": "lmpb9v2lo2lk-source-right",
"targetHandle": "CRT7oIEU8v2P-target-left"
},
{
"source": "CRT7oIEU8v2P",
"target": "i7uow4wj2wdp",
"sourceHandle": "CRT7oIEU8v2P-source-right",
"targetHandle": "i7uow4wj2wdp-target-left"
}
]
}

View File

@@ -10,7 +10,7 @@
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "定义插件输入",
"name": "定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
@@ -24,7 +24,7 @@
},
{
"nodeId": "i7uow4wj2wdp",
"name": "定义插件输出",
"name": "定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",

View File

@@ -10,7 +10,7 @@
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "定义插件输入",
"name": "定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
@@ -77,7 +77,7 @@
},
{
"nodeId": "i7uow4wj2wdp",
"name": "定义插件输出",
"name": "定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
@@ -167,7 +167,7 @@
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"value": "/api/plugins/textEditor"
"value": "/api/plugins/textEditor/v2"
},
{
"key": "system_httpHeader",

View File

@@ -0,0 +1,342 @@
{
"author": "FastGPT Team",
"templateType": "other",
"name": "自定义反馈",
"avatar": "/imgs/module/customFeedback.svg",
"intro": "该模块被触发时,会给当前的对话记录增加一条反馈。可用于自动记录对话效果等。",
"showStatus": false,
"isTool": false,
"weight": 0,
"modules": [
{
"moduleId": "w90mfp",
"name": "自定义插件输入",
"flowType": "pluginInput",
"showStatus": false,
"position": {
"x": 515.1887815471657,
"y": -169.04905809653783
},
"inputs": [
{
"key": "defaultFeedback",
"valueType": "string",
"label": "默认反馈内容",
"type": "textarea",
"required": false,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
},
{
"key": "customFeedback",
"valueType": "string",
"label": "自定义反馈内容",
"type": "target",
"required": false,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
}
],
"outputs": [
{
"key": "defaultFeedback",
"valueType": "string",
"label": "默认反馈内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "defaultFeedback"
}
]
},
{
"key": "customFeedback",
"valueType": "string",
"label": "自定义反馈内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "customFeedback"
}
]
}
]
},
{
"moduleId": "49de3g",
"name": "HTTP模块",
"flowType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1086.8929621216014,
"y": -451.7550009773506
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "system_httpMethod",
"type": "custom",
"valueType": "string",
"label": "",
"value": "POST",
"list": [
{
"label": "GET",
"value": "GET"
},
{
"label": "POST",
"value": "POST"
}
],
"required": true,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpReqUrl",
"type": "hidden",
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "/api/plugins/customFeedback",
"connected": false
},
{
"key": "system_httpHeader",
"type": "custom",
"valueType": "any",
"value": "",
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpParams",
"type": "hidden",
"valueType": "any",
"value": [],
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpJsonBody",
"type": "hidden",
"valueType": "any",
"value": "{\r\n \"appId\": \"{{appId}}\",\r\n \"chatId\": \"{{chatId}}\",\r\n \"responseChatItemId\": \"{{responseChatItemId}}\",\r\n \"defaultFeedback\": \"{{defaultFeedback}}\",\r\n \"customFeedback\": \"{{customFeedback}}\"\r\n}",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.module.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": true,
"hideInApp": true,
"connected": false
},
{
"valueType": "string",
"label": "defaultFeedback",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true,
"key": "defaultFeedback"
},
{
"key": "customFeedback",
"valueType": "string",
"label": "customFeedback",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true
},
{
"key": "system_addInputParam",
"type": "addInputParam",
"valueType": "any",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
},
{
"key": "system_addOutputParam",
"type": "addOutputParam",
"valueType": "any",
"label": "",
"targets": [],
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"outputType": "source",
"valueType": "string"
}
},
{
"type": "source",
"valueType": "string",
"label": "response",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "s15f3v",
"key": "text"
}
],
"key": "response"
}
]
},
{
"moduleId": "s15f3v",
"name": "指定回复",
"flowType": "answerNode",
"position": {
"x": 1705.6337348182756,
"y": -37.53826066726282
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "text",
"type": "textarea",
"valueType": "any",
"label": "core.module.input.label.Response content",
"description": "core.module.input.description.Response content",
"placeholder": "core.module.input.description.Response content",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": true
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
}
]
}
]
}

View File

@@ -0,0 +1,195 @@
{
"author": "FastGPT Team",
"templateType": "tools",
"name": "获取当前时间",
"avatar": "/imgs/module/getCurrentTime.svg",
"intro": "获取用户当前时区的时间。",
"showStatus": false,
"isTool": true,
"weight": 10,
"modules": [
{
"moduleId": "m8dupj",
"name": "自定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/module/input.png",
"flowType": "pluginInput",
"showStatus": false,
"position": {
"x": 187.94161749205568,
"y": 179.78772129776746
},
"inputs": [
{
"key": "pluginStart",
"type": "hidden",
"valueType": "boolean",
"label": "插件开始运行",
"description": "插件开始运行时,会输出一个 True 的标识。有时候,插件不会有额外的的输入,为了顺利的进入下一个阶段,你可以将该值连接到下一个节点的触发器中。",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": true
}
],
"outputs": [
{
"key": "pluginStart",
"label": "插件开始运行",
"type": "source",
"valueType": "boolean",
"targets": [
{
"moduleId": "cv13yt",
"key": "switch"
}
]
}
]
},
{
"moduleId": "bjsa7r",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/module/output.png",
"flowType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1176.9471084832217,
"y": 138.94098316727695
},
"inputs": [
{
"key": "time",
"valueType": "string",
"label": "time",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": false,
"dataType": true,
"inputType": false
},
"connected": true
}
],
"outputs": [
{
"key": "time",
"valueType": "string",
"label": "time",
"type": "source",
"edit": true,
"targets": []
}
]
},
{
"moduleId": "cv13yt",
"name": "文本加工",
"intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。",
"avatar": "/imgs/module/textEditor.svg",
"flowType": "pluginModule",
"showStatus": false,
"position": {
"x": 600.7190079155914,
"y": 1.4754510232677944
},
"inputs": [
{
"key": "pluginId",
"type": "hidden",
"label": "",
"value": "community-textEditor",
"valueType": "string",
"connected": false,
"showTargetInApp": false,
"showTargetInPlugin": false
},
{
"key": "switch",
"type": "triggerAndFinish",
"label": "",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": true
},
{
"key": "textarea",
"valueType": "string",
"label": "文本内容",
"type": "textarea",
"required": true,
"description": "可以通过 {{key}} 的方式引用传入的变量。变量仅支持字符串或数字。",
"edit": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": false,
"placeholder": "可以通过 {{key}} 的方式引用传入的变量。变量仅支持字符串或数字。",
"value": "{{cTime}}"
},
{
"key": "DYNAMIC_INPUT_KEY",
"valueType": "any",
"label": "需要加工的输入",
"type": "addInputParam",
"required": false,
"description": "可动态的添加字符串类型变量,在文本编辑中通过 {{key}} 使用变量。非字符串类型,会自动转成字符串类型。",
"edit": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": false
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
}
],
"outputs": [
{
"key": "text",
"valueType": "string",
"label": "core.module.output.label.text",
"type": "source",
"edit": false,
"targets": [
{
"moduleId": "bjsa7r",
"key": "time"
}
]
},
{
"key": "finish",
"label": "",
"description": "",
"valueType": "boolean",
"type": "hidden",
"targets": []
}
]
}
]
}

View File

@@ -0,0 +1,331 @@
{
"author": "FastGPT Team",
"templateType": "tools",
"name": "文本加工",
"avatar": "/imgs/module/textEditor.svg",
"intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。",
"showStatus": false,
"isTool": false,
"weight": 100,
"modules": [
{
"moduleId": "w90mfp",
"name": "自定义插件输入",
"flowType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [
{
"key": "textarea",
"valueType": "string",
"label": "文本内容",
"type": "textarea",
"required": true,
"description": "可以通过 {{key}} 的方式引用传入的变量。变量仅支持字符串或数字。",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
},
{
"key": "DYNAMIC_INPUT_KEY",
"valueType": "any",
"label": "需要加工的输入",
"type": "addInputParam",
"required": false,
"description": "可动态的添加字符串类型变量,在文本编辑中通过 {{key}} 使用变量。非字符串类型,会自动转成字符串类型。",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": false
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": true
}
],
"outputs": [
{
"key": "textarea",
"valueType": "string",
"label": "文本内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "text"
}
]
},
{
"key": "DYNAMIC_INPUT_KEY",
"valueType": "any",
"label": "需要加工的输入",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "DYNAMIC_INPUT_KEY"
}
]
}
]
},
{
"moduleId": "tze1ju",
"name": "自定义插件输出",
"flowType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -145.93201540017395
},
"inputs": [
{
"key": "text",
"valueType": "string",
"label": "core.module.output.label.text",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": false,
"dataType": true,
"inputType": false
},
"connected": true
}
],
"outputs": [
{
"key": "text",
"valueType": "string",
"label": "core.module.output.label.text",
"type": "source",
"edit": true,
"targets": []
}
]
},
{
"moduleId": "49de3g",
"name": "HTTP模块",
"flowType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1086.8929621216014,
"y": -451.7550009773506
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "system_httpMethod",
"type": "custom",
"valueType": "string",
"label": "",
"value": "POST",
"list": [
{
"label": "GET",
"value": "GET"
},
{
"label": "POST",
"value": "POST"
}
],
"required": true,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpReqUrl",
"type": "hidden",
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "/api/plugins/textEditor",
"connected": false
},
{
"key": "system_httpHeader",
"type": "custom",
"valueType": "any",
"value": "",
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpParams",
"type": "hidden",
"valueType": "any",
"value": [],
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpJsonBody",
"type": "hidden",
"valueType": "any",
"value": "{\r\n \"text\": \"{{text}}\"\r\n}",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.module.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": true,
"hideInApp": true,
"connected": true
},
{
"key": "text",
"valueType": "string",
"label": "text",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true
},
{
"key": "system_addInputParam",
"type": "addInputParam",
"valueType": "any",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
},
{
"key": "system_addOutputParam",
"type": "addOutputParam",
"valueType": "any",
"label": "",
"targets": [],
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"outputType": "source",
"valueType": "string"
}
},
{
"type": "source",
"valueType": "string",
"key": "text",
"label": "core.module.output.label.text",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "tze1ju",
"key": "text"
}
]
}
]
}
]
}

View File

@@ -0,0 +1,369 @@
{
"abandon": true,
"author": "FastGPT Team",
"templateType": "tools",
"name": "判断器",
"avatar": "/imgs/module/tfSwitch.svg",
"intro": "根据传入的内容进行 True False 输出。默认情况下,当传入的内容为 false, undefined, null, 0, none 时,会输出 false。你也可以增加一些自定义的字符串来补充输出 false 的内容。非字符、非数字、非布尔类型,直接输出 True。",
"showStatus": false,
"isTool": false,
"weight": 10,
"modules": [
{
"moduleId": "w90mfp",
"name": "自定义插件输入",
"flowType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [
{
"key": "input",
"valueType": "any",
"type": "target",
"label": "core.module.input.label.TFSwitch input tip",
"required": true,
"edit": true,
"connected": true
},
{
"key": "rule",
"valueType": "string",
"label": "core.module.input.label.TFSwitch textarea",
"type": "textarea",
"required": false,
"description": "core.module.input.description.TFSwitch textarea",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
}
],
"outputs": [
{
"key": "input",
"valueType": "any",
"label": "core.module.input.label.TFSwitch input tip",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "8kld99",
"key": "input"
}
]
},
{
"key": "rule",
"valueType": "string",
"label": "core.module.input.label.TFSwitch textarea",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "8kld99",
"key": "rule"
}
]
}
]
},
{
"moduleId": "tze1ju",
"name": "自定义插件输出",
"flowType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1985.3791673445353,
"y": -144.90535546692078
},
"inputs": [
{
"key": "true",
"type": "target",
"valueType": "boolean",
"label": "True",
"required": true,
"edit": true,
"connected": true,
"description": ""
},
{
"key": "false",
"valueType": "boolean",
"label": "False",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": false,
"dataType": true,
"inputType": false
},
"connected": true
}
],
"outputs": [
{
"key": "true",
"valueType": "boolean",
"label": "True",
"type": "source",
"edit": true,
"targets": []
},
{
"key": "false",
"valueType": "boolean",
"label": "False",
"type": "source",
"edit": true,
"targets": []
}
]
},
{
"moduleId": "8kld99",
"name": "HTTP模块",
"flowType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1210.560012858087,
"y": -387.62433050951756
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "system_httpMethod",
"type": "custom",
"valueType": "string",
"label": "",
"value": "POST",
"list": [
{
"label": "GET",
"value": "GET"
},
{
"label": "POST",
"value": "POST"
}
],
"required": true,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpReqUrl",
"type": "hidden",
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "/api/plugins/TFSwitch",
"connected": false
},
{
"key": "system_httpHeader",
"type": "custom",
"valueType": "any",
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpParams",
"type": "hidden",
"valueType": "any",
"value": [],
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpJsonBody",
"type": "hidden",
"valueType": "any",
"value": "{\r\n \"input\": \"{{input}}\",\r\n \"rule\": \"{{rule}}\"\r\n}",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.module.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": true,
"hideInApp": true,
"connected": false
},
{
"key": "input",
"valueType": "any",
"label": "input",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true
},
{
"key": "rule",
"valueType": "string",
"label": "rule",
"type": "target",
"required": false,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true
},
{
"key": "system_addInputParam",
"type": "addInputParam",
"valueType": "any",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
},
{
"key": "system_addOutputParam",
"type": "addOutputParam",
"valueType": "any",
"label": "",
"targets": [],
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"outputType": "source",
"valueType": "string"
}
},
{
"type": "source",
"valueType": "boolean",
"key": "true",
"label": "true",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "tze1ju",
"key": "true"
}
]
},
{
"type": "source",
"valueType": "boolean",
"key": "false",
"label": "false",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "tze1ju",
"key": "false"
}
]
}
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,11 @@
{
"App": "应用",
"Create New": "",
"Export": "导出",
"Folder": "文件夹",
"Move": "移动",
"Name": "名称",
"Rename": "重命名",
"Running": "运行中",
"Select value is empty": "选择的内容为空",
"UnKnow": "未知",
"Warning": "提示",
"app": {
@@ -646,8 +644,7 @@
"success": "开始同步"
}
},
"training": {
}
"training": {}
},
"data": {
"Auxiliary Data": "辅助数据",
@@ -922,7 +919,7 @@
"AppId": "应用的ID",
"ChatId": "当前对话ID",
"Current time": "当前时间",
"Histories": "历史记录,最多取10条",
"Histories": "最近10条聊天记录",
"Key already exists": "Key 已经存在",
"Key cannot be empty": "参数名不能为空",
"Props name": "参数名",
@@ -939,6 +936,7 @@
"Add Input": "添加入参",
"Input Number": "入参: {{length}}",
"add": "添加条件",
"Add Branch": "添加分支",
"description": {
"Background": "你可以添加一些特定内容的介绍,从而更好的识别用户的问题类型。这个内容通常是给模型介绍一个它不知道的内容。",
"HTTP Dynamic Input": "接收前方节点的输出值作为变量这些变量可以被HTTP请求参数使用。",
@@ -975,6 +973,17 @@
"Classify background": "例如: \n1. AIGC人工智能生成内容是指使用人工智能技术自动或半自动地生成数字内容如文本、图像、音乐、视频等。\n2. AIGC技术包括但不限于自然语言处理、计算机视觉、机器学习和深度学习。这些技术可以创建新内容或修改现有内容以满足特定的创意、教育、娱乐或信息需求。"
}
},
"inputType": {
"chat history": "",
"dynamicTargetInput": "",
"input": "",
"selectApp": "",
"selectDataset": "",
"selectLLMModel": "",
"switch": "",
"target": "",
"textarea": ""
},
"laf": {
"Select laf function": "选择laf函数"
},
@@ -1069,7 +1078,7 @@
"tools": "工具调用"
},
"variable": {
"External type": "外部传入",
"Custom type": "自定义变量",
"add option": "添加选项",
"input type": "文本",
"key": "变量 key",
@@ -1120,6 +1129,9 @@
"Stop debug": "停止调试",
"Success": "运行成功",
"Value type": "数据类型",
"Variable": {
"Variable type": "变量类型"
},
"Variable outputs": "全局变量",
"chat": {
"Quote prompt": "引用提示词"
@@ -1154,13 +1166,15 @@
},
"publish": {
"OnRevert version": "点击回退到该版本",
"OnRevert version confirm": "确认回退该版本?",
"OnRevert version confirm": "确认回退该版本?会为您保存编辑中版本的配置,并为回退版本创建一个新的发布版本。",
"histories": "发布记录"
},
"tool": {
"Handle": "工具连接器",
"Select Tool": "选择工具"
}
},
"value": "值",
"variable": "变量"
}
},
"dataset": {
@@ -1189,7 +1203,6 @@
"Select Dataset": "选择该知识库",
"Select Dataset Tips": "仅能选择同一个索引模型的知识库",
"Select Folder": "进入文件夹",
"System Data Queue": "排队长度",
"Training Name": "数据训练",
"Upload Time": "上传时间",
"collections": {

View File

@@ -1,5 +1,5 @@
import { VariableItemType } from '@fastgpt/global/core/app/type.d';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react';
@@ -24,10 +24,23 @@ const VariableInput = ({
chatForm: UseFormReturn<ChatBoxInputFormType>;
}) => {
const { t } = useTranslation();
const [refresh, setRefresh] = useState(false);
const { register, setValue, handleSubmit: handleSubmitChat, watch } = chatForm;
const { register, unregister, setValue, handleSubmit: handleSubmitChat, watch } = chatForm;
const variables = watch('variables');
useEffect(() => {
// 重新注册所有字段
variableModules.forEach((item) => {
register(`variables.${item.key}`, { required: item.required });
});
return () => {
// 组件卸载时注销所有字段
variableModules.forEach((item) => {
unregister(`variables.${item.key}`);
});
};
}, [register, unregister, variableModules]);
return (
<Box py={3}>
{/* avatar */}
@@ -92,7 +105,6 @@ const VariableInput = ({
value={variables[item.key]}
onchange={(e) => {
setValue(`variables.${item.key}`, e);
setRefresh((state) => !state);
}}
/>
)}
@@ -116,4 +128,4 @@ const VariableInput = ({
);
};
export default React.memo(VariableInput);
export default VariableInput;

View File

@@ -91,6 +91,7 @@ type Props = OutLinkChatAuthProps & {
onStartChat?: (e: StartChatFnProps) => Promise<{
responseText: string;
[DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[];
newVariables?: Record<string, any>;
isNewChat?: boolean;
}>;
onDelMessage?: (e: { contentId: string }) => void;
@@ -157,12 +158,6 @@ const ChatBox = (
isChatting
} = useChatProviderStore();
/* variable */
const filterVariableModules = useMemo(
() => variableModules.filter((item) => item.type !== VariableInputEnum.external),
[variableModules]
);
// compute variable input is finish.
const chatForm = useForm<ChatBoxInputFormType>({
defaultValues: {
@@ -173,9 +168,15 @@ const ChatBox = (
}
});
const { setValue, watch, handleSubmit } = chatForm;
const variables = watch('variables');
const chatStarted = watch('chatStarted');
const variableIsFinish = useMemo(() => {
/* variable */
const variables = watch('variables');
const filterVariableModules = useMemo(
() => variableModules.filter((item) => item.type !== VariableInputEnum.custom),
[variableModules]
);
const variableIsFinish = (() => {
if (!filterVariableModules || filterVariableModules.length === 0 || chatHistories.length > 0)
return true;
@@ -187,7 +188,7 @@ const ChatBox = (
}
return chatStarted;
}, [filterVariableModules, chatHistories.length, chatStarted, variables]);
})();
// 滚动到底部
const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => {
@@ -359,6 +360,12 @@ const ChatBox = (
[questionGuide, shareId, outLinkUid, teamId, teamToken]
);
/* Abort chat completions, questionGuide */
const abortRequest = useCallback(() => {
chatController.current?.abort('stop');
questionGuideController.current?.abort('stop');
}, []);
/**
* user confirm send prompt
*/
@@ -382,6 +389,8 @@ const ChatBox = (
return;
}
abortRequest();
text = text.trim();
if (!text && files.length === 0) {
@@ -462,6 +471,7 @@ const ChatBox = (
const {
responseData,
responseText,
newVariables,
isNewChat = false
} = await onStartChat({
chatList: newChatList,
@@ -471,6 +481,8 @@ const ChatBox = (
variables
});
newVariables && setValue('variables', newVariables);
isNewChatReplace.current = isNewChat;
// set finish status
@@ -537,6 +549,7 @@ const ChatBox = (
})();
},
[
abortRequest,
chatHistories,
createQuestionGuide,
finishSegmentedAudio,
@@ -549,6 +562,7 @@ const ChatBox = (
resetInputVal,
setAudioPlayingChatId,
setChatHistories,
setValue,
splitText2Audio,
startSegmentedAudio,
t,
@@ -706,7 +720,7 @@ const ChatBox = (
});
};
},
[appId, chatId, feedbackType, teamId, teamToken]
[appId, chatId, feedbackType, setChatHistories, teamId, teamToken]
);
const onADdUserDislike = useCallback(
(chat: ChatSiteItemType) => {
@@ -743,7 +757,7 @@ const ChatBox = (
return () => setFeedbackId(chat.dataId);
}
},
[appId, chatId, feedbackType, outLinkUid, shareId, teamId, teamToken]
[appId, chatId, feedbackType, outLinkUid, setChatHistories, shareId, teamId, teamToken]
);
const onReadUserDislike = useCallback(
(chat: ChatSiteItemType) => {
@@ -864,6 +878,7 @@ const ChatBox = (
setValue('variables', e || defaultVal);
},
resetHistory(e) {
abortRequest();
setValue('chatStarted', e.length > 0);
setChatHistories(e);
},

View File

@@ -17,13 +17,13 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { getFileAndOpen } from '@/web/core/dataset/utils';
import { MARKDOWN_QUOTE_SIGN } from '@fastgpt/global/core/chat/constants';
const CodeLight = dynamic(() => import('./CodeLight'));
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'));
const MdImage = dynamic(() => import('./img/Image'));
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'));
const CodeLight = dynamic(() => import('./CodeLight'), { ssr: false });
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
const MdImage = dynamic(() => import('./img/Image'), { ssr: false });
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false });
const ChatGuide = dynamic(() => import('./chat/Guide'));
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'));
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
export enum CodeClassName {
guide = 'guide',

View File

@@ -173,7 +173,7 @@ const VariableEdit = ({
maxW={['90vw', '500px']}
>
<ModalBody>
{variableType !== VariableInputEnum.external && (
{variableType !== VariableInputEnum.custom && (
<Flex alignItems={'center'}>
<Box w={'70px'}>{t('common.Require Input')}</Box>
<Switch {...registerEdit('variable.required')} />
@@ -197,7 +197,7 @@ const VariableEdit = ({
</Flex>
<Box mt={5} mb={2}>
{t('core.module.Field Type')}
{t('core.workflow.Variable.Variable type')}
</Box>
<MyRadio
gridGap={4}

View File

@@ -65,10 +65,10 @@ const ChatTest = (
}
});
});
const history = chatList.slice(-historyMaxLen - 2, -2);
const history = chatList.slice(-(historyMaxLen * 2) - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({
const { responseText, responseData, newVariables } = await streamFetch({
url: '/api/core/chat/chatTest',
data: {
history,
@@ -84,7 +84,7 @@ const ChatTest = (
abortCtrl: controller
});
return { responseText, responseData };
return { responseText, responseData, newVariables };
},
[app._id, app.name, edges, nodes]
);
@@ -99,7 +99,7 @@ const ChatTest = (
return (
<>
<Flex
zIndex={3}
zIndex={101}
flexDirection={'column'}
position={'absolute'}
top={5}

View File

@@ -57,7 +57,8 @@ const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = {
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
),
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse'))
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge

View File

@@ -90,7 +90,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'transparentBase'}
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}

View File

@@ -1,381 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from './render/NodeCard';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex, background } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import MyIcon from '@fastgpt/web/components/common/Icon';
import RenderOutput from './render/RenderOutput';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import {
IfElseConditionType,
IfElseListItemType
} from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import {
VariableConditionEnum,
allConditionList,
arrayConditionList,
booleanConditionList,
numberConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { stringConditionList } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyInput from '@/components/MyInput';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs = [], outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const condition = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.condition)
?.value as IfElseConditionType) || 'OR',
[inputs]
);
const ifElseList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList)
?.value as IfElseListItemType[]) || [],
[inputs]
);
const onUpdateIfElseList = useCallback(
(value: IfElseListItemType[]) => {
const ifElseListInput = inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList);
if (!ifElseListInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.ifElseList,
value: {
...ifElseListInput,
value
}
});
},
[inputs, nodeId, onChangeNode]
);
const RenderAddCondition = useMemo(() => {
return (
<Button
onClick={() => {
onUpdateIfElseList([
...ifElseList,
{
variable: undefined,
condition: undefined,
value: undefined
}
]);
}}
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
my={3}
w={'full'}
>
{t('core.module.input.add')}
</Button>
);
}, [ifElseList, onUpdateIfElseList, t]);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={6}>
<RenderOutput nodeId={nodeId} flowOutputList={[outputs[0]]} />
</Box>
<Box py={3} px={4}>
<Box className="nowheel">
{ifElseList.map((item, i) => {
return (
<Box key={i}>
{/* border */}
{i !== 0 && (
<Flex alignItems={'center'} w={'full'} py={'5px'}>
<Box
w={'auto'}
flex={1}
height={'1px'}
style={{
background:
'linear-gradient(90deg, rgba(232, 235, 240, 0.00) 0%, #E8EBF0 100%)'
}}
></Box>
<Flex
px={'2.5'}
color={'primary.600'}
fontWeight={'medium'}
alignItems={'center'}
cursor={'pointer'}
rounded={'md'}
onClick={() => {
const conditionInput = inputs.find(
(input) => input.key === NodeInputKeyEnum.condition
);
if (!conditionInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.condition,
value: {
...conditionInput,
value: conditionInput.value === 'OR' ? 'AND' : 'OR'
}
});
}}
>
{condition}
<MyIcon ml={1} boxSize={5} name="change" />
</Flex>
<Box
w={'auto'}
flex={1}
height={'1px'}
style={{
background:
'linear-gradient(90deg, #E8EBF0 0%, rgba(232, 235, 240, 0.00) 100%)'
}}
></Box>
</Flex>
)}
{/* condition list */}
<Flex gap={2} alignItems={'center'}>
{/* variable reference */}
<Box minW={'250px'}>
<Reference
nodeId={nodeId}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === i) {
return {
...ifElse,
variable: e
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* condition select */}
<Box w={'130px'} flex={1}>
<ConditionSelect
condition={item.condition}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === i) {
return {
...ifElse,
condition: e
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* value */}
<Box w={'200px'}>
<ConditionValueInput
value={item.value}
condition={item.condition}
variable={item.variable}
onChange={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === i) {
return {
...ifElse,
value: e
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* delete */}
{ifElseList.length > 1 && (
<MyIcon
ml={2}
boxSize={5}
name="delete"
cursor={'pointer'}
_hover={{ color: 'red.600' }}
color={'myGray.400'}
onClick={() => {
onUpdateIfElseList(ifElseList.filter((_, index) => index !== i));
}}
/>
)}
</Flex>
</Box>
);
})}
</Box>
{RenderAddCondition}
</Box>
<Box px={6} mb={4}>
<RenderOutput nodeId={nodeId} flowOutputList={[outputs[1]]} />
</Box>
</NodeCard>
);
};
export default React.memo(NodeIfElse);
const Reference = ({
nodeId,
variable,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
onSelect: (e: ReferenceValueProps) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
nodeId,
valueType: WorkflowIOValueTypeEnum.any,
value: variable
});
return (
<ReferSelector
placeholder={t('选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};
/* Different data types have different options */
const ConditionSelect = ({
condition,
variable,
onSelect
}: {
condition?: VariableConditionEnum;
variable?: ReferenceValueProps;
onSelect: (e: VariableConditionEnum) => void;
}) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// get condition type
const valueType = useMemo(() => {
if (!variable) return;
const node = nodeList.find((node) => node.nodeId === variable[0]);
if (!node) return WorkflowIOValueTypeEnum.any;
const output = node.outputs.find((item) => item.id === variable[1]);
if (!output) return WorkflowIOValueTypeEnum.any;
return output.valueType;
}, [nodeList, variable]);
const conditionList = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList;
if (valueType === WorkflowIOValueTypeEnum.number) return numberConditionList;
if (valueType === WorkflowIOValueTypeEnum.boolean) return booleanConditionList;
if (
valueType === WorkflowIOValueTypeEnum.chatHistory ||
valueType === WorkflowIOValueTypeEnum.datasetQuote ||
valueType === WorkflowIOValueTypeEnum.dynamic ||
valueType === WorkflowIOValueTypeEnum.selectApp ||
valueType === WorkflowIOValueTypeEnum.arrayBoolean ||
valueType === WorkflowIOValueTypeEnum.arrayNumber ||
valueType === WorkflowIOValueTypeEnum.arrayObject ||
valueType === WorkflowIOValueTypeEnum.arrayString ||
valueType === WorkflowIOValueTypeEnum.object
)
return arrayConditionList;
if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList;
return [];
}, [valueType]);
return (
<MySelect
w={'100%'}
list={conditionList}
value={condition}
onchange={onSelect}
placeholder="选择条件"
></MySelect>
);
};
/*
Different condition can be entered differently
empty, notEmpty: forbid input
boolean type: select true/false
*/
const ConditionValueInput = ({
value = '',
variable,
condition,
onChange
}: {
value?: string;
variable?: ReferenceValueProps;
condition?: VariableConditionEnum;
onChange: (e: string) => void;
}) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// get value type
const valueType = useMemo(() => {
if (!variable) return;
const node = nodeList.find((node) => node.nodeId === variable[0]);
if (!node) return WorkflowIOValueTypeEnum.any;
const output = node.outputs.find((item) => item.id === variable[1]);
if (!output) return WorkflowIOValueTypeEnum.any;
return output.valueType;
}, [nodeList, variable]);
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<MySelect
list={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' }
]}
onchange={onChange}
value={value}
placeholder={'选择值'}
/>
);
} else {
return (
<MyInput
value={value}
placeholder={'输入值'}
w={'100%'}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
onChange={(e) => onChange(e.target.value)}
/>
);
}
};

View File

@@ -0,0 +1,415 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import {
DraggableProvided,
DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag/index';
import Container from '../../components/Container';
import { DragHandleIcon, MinusIcon, SmallAddIcon } from '@chakra-ui/icons';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { useTranslation } from 'react-i18next';
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
VariableConditionEnum,
allConditionList,
arrayConditionList,
booleanConditionList,
numberConditionList,
stringConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { useContextSelector } from 'use-context-selector';
import React, { useMemo } from 'react';
import { WorkflowContext } from '../../../context';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyInput from '@/components/MyInput';
import { getElseIFLabel, getHandleId } from '@fastgpt/global/core/workflow/utils';
import { SourceHandle } from '../render/Handle';
import { Position, useReactFlow } from 'reactflow';
import { getReferenceDataValueType } from '@/web/core/workflow/utils';
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
const ListItem = ({
provided,
snapshot,
conditionIndex,
conditionItem,
ifElseList,
onUpdateIfElseList,
nodeId
}: {
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
conditionIndex: number;
conditionItem: IfElseListItemType;
ifElseList: IfElseListItemType[];
onUpdateIfElseList: (value: IfElseListItemType[]) => void;
nodeId: string;
}) => {
const { t } = useTranslation();
const { getZoom } = useReactFlow();
return (
<Box
ref={provided.innerRef}
{...provided.draggableProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
>
<Flex
alignItems={'center'}
position={'relative'}
transform={snapshot.isDragging ? `scale(${getZoom()})` : ''}
transformOrigin={'top left'}
>
<Container w={snapshot.isDragging ? '' : 'full'} className="nodrag">
<Flex mb={4} alignItems={'center'}>
{ifElseList.length > 1 && <DragIcon provided={provided} />}
<Box color={'black'} fontSize={'lg'} ml={2}>
{getElseIFLabel(conditionIndex)}
</Box>
{conditionItem.list?.length > 1 && (
<Flex
px={'2.5'}
color={'primary.600'}
fontWeight={'medium'}
alignItems={'center'}
cursor={'pointer'}
rounded={'md'}
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
condition: ifElse.condition === 'AND' ? 'OR' : 'AND'
};
}
return ifElse;
})
);
}}
>
{conditionItem.condition}
<MyIcon ml={1} boxSize={5} name="change" />
</Flex>
)}
<Box flex={1}></Box>
{ifElseList.length > 1 && (
<MyIcon
ml={2}
boxSize={5}
name="delete"
cursor={'pointer'}
_hover={{ color: 'red.600' }}
color={'myGray.400'}
onClick={() => {
onUpdateIfElseList(ifElseList.filter((_, index) => index !== conditionIndex));
}}
/>
)}
</Flex>
<Box>
{conditionItem.list?.map((item, i) => {
return (
<Box key={i}>
{/* condition list */}
<Flex gap={2} mb={2} alignItems={'center'}>
{/* variable reference */}
<Box minW={'250px'}>
<Reference
nodeId={nodeId}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
variable: e
};
}
return item;
})
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* condition select */}
<Box w={'130px'} flex={1}>
<ConditionSelect
condition={item.condition}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
condition: e
};
}
return item;
})
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* value */}
<Box w={'200px'}>
<ConditionValueInput
value={item.value}
condition={item.condition}
variable={item.variable}
onChange={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
value: e
};
}
return item;
})
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* delete */}
{conditionItem.list.length > 1 && (
<MinusIcon
ml={2}
boxSize={3}
name="delete"
cursor={'pointer'}
_hover={{ color: 'red.600' }}
color={'myGray.400'}
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.filter((_, index) => index !== i)
};
}
return ifElse;
})
);
}}
/>
)}
</Flex>
</Box>
);
})}
</Box>
<Button
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.concat({
variable: undefined,
condition: undefined,
value: undefined
})
};
}
return ifElse;
})
);
}}
variant={'link'}
leftIcon={<SmallAddIcon />}
my={3}
color={'primary.600'}
>
{t('core.module.input.add')}
</Button>
</Container>
{!snapshot.isDragging && (
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', getElseIFLabel(conditionIndex))}
position={Position.Right}
translate={[18, 0]}
/>
)}
</Flex>
</Box>
);
};
export default React.memo(ListItem);
const Reference = ({
nodeId,
variable,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
onSelect: (e: ReferenceValueProps) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
nodeId,
valueType: WorkflowIOValueTypeEnum.any,
value: variable
});
return (
<ReferSelector
placeholder={t('选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};
/* Different data types have different options */
const ConditionSelect = ({
condition,
variable,
onSelect
}: {
condition?: VariableConditionEnum;
variable?: ReferenceValueProps;
onSelect: (e: VariableConditionEnum) => void;
}) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// get condition type
const valueType = useMemo(() => {
return getReferenceDataValueType({
variable,
nodeList,
t
});
}, [nodeList, t, variable]);
const conditionList = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList;
if (valueType === WorkflowIOValueTypeEnum.number) return numberConditionList;
if (valueType === WorkflowIOValueTypeEnum.boolean) return booleanConditionList;
if (
valueType === WorkflowIOValueTypeEnum.chatHistory ||
valueType === WorkflowIOValueTypeEnum.datasetQuote ||
valueType === WorkflowIOValueTypeEnum.dynamic ||
valueType === WorkflowIOValueTypeEnum.selectApp ||
valueType === WorkflowIOValueTypeEnum.arrayBoolean ||
valueType === WorkflowIOValueTypeEnum.arrayNumber ||
valueType === WorkflowIOValueTypeEnum.arrayObject ||
valueType === WorkflowIOValueTypeEnum.arrayString ||
valueType === WorkflowIOValueTypeEnum.object
)
return arrayConditionList;
if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList;
return [];
}, [valueType]);
return (
<MySelect
w={'100%'}
list={conditionList}
value={condition}
onchange={onSelect}
placeholder="选择条件"
/>
);
};
/*
Different condition can be entered differently
empty, notEmpty: forbid input
boolean type: select true/false
*/
const ConditionValueInput = ({
value = '',
variable,
condition,
onChange
}: {
value?: string;
variable?: ReferenceValueProps;
condition?: VariableConditionEnum;
onChange: (e: string) => void;
}) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// get value type
const valueType = useMemo(() => {
if (!variable) return;
const node = nodeList.find((node) => node.nodeId === variable[0]);
if (!node) return WorkflowIOValueTypeEnum.any;
const output = node.outputs.find((item) => item.id === variable[1]);
if (!output) return WorkflowIOValueTypeEnum.any;
return output.valueType;
}, [nodeList, variable]);
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<MySelect
list={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' }
]}
onchange={onChange}
value={value}
placeholder={'选择值'}
/>
);
} else {
return (
<MyInput
value={value}
placeholder={'输入值'}
w={'100%'}
bg={'white'}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
onChange={(e) => onChange(e.target.value)}
/>
);
}
};

View File

@@ -0,0 +1,136 @@
import React, { useCallback, useMemo, useState } from 'react';
import NodeCard from '../render/NodeCard';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex } from '@chakra-ui/react';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeProps, Position } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import Container from '../../components/Container';
import DndDrag, { Draggable, DropResult } from '@fastgpt/web/components/common/DndDrag/index';
import { SourceHandle } from '../render/Handle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import ListItem from './ListItem';
import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs = [] } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const ifElseList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList)
?.value as IfElseListItemType[]) || [],
[inputs]
);
const onUpdateIfElseList = useCallback(
(value: IfElseListItemType[]) => {
const ifElseListInput = inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList);
if (!ifElseListInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.ifElseList,
value: {
...ifElseListInput,
value
}
});
},
[inputs, nodeId, onChangeNode]
);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} cursor={'default'}>
<DndDrag<IfElseListItemType>
onDragEndCb={(list) => onUpdateIfElseList(list)}
dataList={ifElseList}
renderClone={(provided, snapshot, rubric) => (
<ListItem
provided={provided}
snapshot={snapshot}
conditionItem={ifElseList[rubric.source.index]}
conditionIndex={rubric.source.index}
ifElseList={ifElseList}
onUpdateIfElseList={onUpdateIfElseList}
nodeId={nodeId}
/>
)}
>
{(provided) => (
<Box {...provided.droppableProps} ref={provided.innerRef}>
{ifElseList.map((conditionItem, conditionIndex) => (
<Draggable
key={conditionIndex}
draggableId={conditionIndex.toString()}
index={conditionIndex}
>
{(provided, snapshot) => (
<ListItem
provided={provided}
snapshot={snapshot}
conditionItem={conditionItem}
conditionIndex={conditionIndex}
ifElseList={ifElseList}
onUpdateIfElseList={onUpdateIfElseList}
nodeId={nodeId}
/>
)}
</Draggable>
))}
</Box>
)}
</DndDrag>
<Container position={'relative'}>
<Flex alignItems={'center'}>
<Box color={'black'} fontSize={'lg'} ml={2}>
{IfElseResultEnum.ELSE}
</Box>
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', IfElseResultEnum.ELSE)}
position={Position.Right}
translate={[26, 0]}
/>
</Flex>
</Container>
</Box>
<Box py={3} px={6}>
<Button
variant={'whiteBase'}
w={'full'}
onClick={() => {
const ifElseListInput = inputs.find(
(input) => input.key === NodeInputKeyEnum.ifElseList
);
if (!ifElseListInput) return;
onUpdateIfElseList([
...ifElseList,
{
condition: 'AND',
list: [
{
variable: undefined,
condition: undefined,
value: undefined
}
]
}
]);
}}
>
{t('core.module.input.Add Branch')}
</Button>
</Box>
</NodeCard>
);
};
export default React.memo(NodeIfElse);

View File

@@ -0,0 +1,309 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from './render/NodeCard';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { useTranslation } from 'react-i18next';
import {
Box,
Button,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { SmallAddIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import { getReferenceDataValueType } from '@/web/core/workflow/utils';
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const updateList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.updateList)
?.value as TUpdateListItem[]) || [],
[inputs]
);
const onUpdateList = useCallback(
(value: TUpdateListItem[]) => {
const updateListInput = inputs.find((input) => input.key === NodeInputKeyEnum.updateList);
if (!updateListInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.updateList,
value: {
...updateListInput,
value
}
});
},
[inputs, nodeId, onChangeNode]
);
const Render = useMemo(() => {
const menuList = [
{
renderType: FlowNodeInputTypeEnum.input,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon,
label: t('core.workflow.inputType.Manual input')
},
{
renderType: FlowNodeInputTypeEnum.reference,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon,
label: t('core.workflow.inputType.Reference')
}
];
return (
<>
{updateList.map((updateItem, index) => {
const valueType = getReferenceDataValueType({
variable: updateItem.variable,
nodeList,
t
});
const renderTypeData = menuList.find((item) => item.renderType === updateItem.renderType);
const handleUpdate = (newValue: ReferenceValueProps | string) => {
if (isReferenceValue(newValue)) {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: newValue as ReferenceValueProps } : update
)
);
} else {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: ['', newValue as string] } : update
)
);
}
};
return (
<Container key={index} mt={4} w={'full'} mx={0}>
<Flex alignItems={'center'}>
<Flex w={'60px'}>{t('core.workflow.variable')}</Flex>
<Reference
nodeId={nodeId}
variable={updateItem.variable}
onSelect={(value) => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
valueType,
variable: value
};
}
return update;
})
);
}}
/>
<Box flex={1} />
{updateList.length > 1 && (
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
top={3}
right={3}
onClick={() => {
onUpdateList(updateList.filter((_, i) => i !== index));
}}
/>
)}
</Flex>
<Flex mt={2} w={'full'} alignItems={'center'} className="nodrag">
<Flex w={'60px'}>
<Box>{t('core.workflow.value')}</Box>
<MyTooltip
label={
menuList.find((item) => item.renderType === updateItem.renderType)?.label
}
>
<Button
size={'xs'}
bg={'white'}
borderRadius={'xs'}
mx={2}
onClick={() => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
renderType:
updateItem.renderType === FlowNodeInputTypeEnum.input
? FlowNodeInputTypeEnum.reference
: FlowNodeInputTypeEnum.input
};
}
return update;
})
);
}}
>
<MyIcon name={renderTypeData?.icon as any} w={'14px'} />
</Button>
</MyTooltip>
</Flex>
{/* Render input components */}
{(() => {
if (updateItem.renderType === FlowNodeInputTypeEnum.reference) {
return (
<Reference
nodeId={nodeId}
variable={updateItem.value}
valueType={valueType}
onSelect={handleUpdate}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
bg="white"
value={updateItem.value?.[1] || ''}
w="300px"
onChange={(e) => handleUpdate(e.target.value)}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput value={Number(updateItem.value?.[1]) || 0}>
<NumberInputField
bg="white"
onChange={(e) => handleUpdate(e.target.value)}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
size="lg"
defaultChecked={updateItem.value?.[1] === 'true'}
onChange={(e) => handleUpdate(String(e.target.checked))}
/>
);
}
return (
<JsonEditor
bg="white"
resize
w="300px"
value={String(updateItem.value?.[1] || '')}
onChange={(e) => {
handleUpdate(e);
}}
/>
);
})()}
</Flex>
</Container>
);
})}
</>
);
}, [nodeId, nodeList, onUpdateList, t, updateList]);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} pb={4}>
{Render}
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
w={'full'}
size={'sm'}
onClick={() => {
onUpdateList([
...updateList,
{
variable: ['', ''],
value: ['', ''],
renderType: FlowNodeInputTypeEnum.input
}
]);
}}
>
{t('common.Add New')}
</Button>
</Flex>
</Box>
</NodeCard>
);
};
export default React.memo(NodeVariableUpdate);
const Reference = ({
nodeId,
variable,
valueType,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
valueType?: WorkflowIOValueTypeEnum;
onSelect: (e: ReferenceValueProps) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
nodeId,
valueType,
value: variable
});
return (
<ReferSelector
placeholder={t('选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};

View File

@@ -38,6 +38,8 @@ type Props = FlowNodeItemType & {
const NodeCard = (props: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
const {
children,
avatar = LOGO_ICON,
@@ -59,6 +61,13 @@ const NodeCard = (props: Props) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
// custom title edit
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const showToolHandle = useMemo(
() => isTool && !!nodeList.find((item) => item?.flowNodeType === FlowNodeTypeEnum.tools),
@@ -70,7 +79,6 @@ const NodeCard = (props: Props) => {
return (
<Box position={'relative'}>
{/* debug */}
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
<Box className="custom-drag-handle" px={4} py={3}>
{/* tool target handle */}
{showToolHandle && <ToolTargetHandle nodeId={nodeId} />}
@@ -81,13 +89,42 @@ const NodeCard = (props: Props) => {
<Box ml={3} fontSize={'lg'} fontWeight={'medium'}>
{t(name)}
</Box>
{!menuForbid?.rename && (
<MyIcon
className="controller-rename"
display={'none'}
name={'edit'}
w={'14px'}
cursor={'pointer'}
ml={1}
color={'myGray.500'}
_hover={{ color: 'primary.600' }}
onClick={() => {
onOpenCustomTitleModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app.modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
nodeId,
type: 'attr',
key: 'name',
value: e
});
}
});
}}
/>
)}
</Flex>
<MenuRender
name={name}
nodeId={nodeId}
pluginId={pluginId}
flowNodeType={flowNodeType}
inputs={inputs}
menuForbid={menuForbid}
/>
<NodeIntro nodeId={nodeId} intro={intro} />
@@ -96,16 +133,17 @@ const NodeCard = (props: Props) => {
);
}, [
nodeId,
debugResult,
showToolHandle,
avatar,
t,
name,
menuForbid,
pluginId,
flowNodeType,
inputs,
menuForbid,
intro
intro,
onOpenCustomTitleModal,
onChangeNode,
toast
]);
return (
@@ -123,6 +161,9 @@ const NodeCard = (props: Props) => {
},
'& .controller-debug': {
display: 'block'
},
'& .controller-rename': {
display: 'block'
}
}}
onMouseEnter={() => setHoverNodeId(nodeId)}
@@ -136,10 +177,13 @@ const NodeCard = (props: Props) => {
borderColor: selected ? 'primary.600' : 'borderColor.base'
})}
>
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
{Header}
{children}
<ConnectionSourceHandle nodeId={nodeId} />
<ConnectionTargetHandle nodeId={nodeId} />
<EditTitleModal maxLength={20} />
</Box>
);
};
@@ -147,18 +191,14 @@ const NodeCard = (props: Props) => {
export default React.memo(NodeCard);
const MenuRender = React.memo(function MenuRender({
name,
nodeId,
pluginId,
flowNodeType,
inputs,
menuForbid
}: {
name: string;
nodeId: string;
pluginId?: string;
flowNodeType: Props['flowNodeType'];
inputs: Props['inputs'];
menuForbid?: Props['menuForbid'];
}) {
const { t } = useTranslation();
@@ -169,11 +209,7 @@ const MenuRender = React.memo(function MenuRender({
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
content: t('module.Confirm Sync Plugin')
});
// custom title edit
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
@@ -182,7 +218,6 @@ const MenuRender = React.memo(function MenuRender({
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onCopyNode = useCallback(
(nodeId: string) => {
@@ -223,6 +258,22 @@ const MenuRender = React.memo(function MenuRender({
},
[setEdges, setNodes]
);
const onclickSyncVersion = useCallback(async () => {
if (!pluginId) return;
try {
setLoading(true);
onResetNode({
id: nodeId,
node: await getPreviewPluginModule(pluginId)
});
} catch (e) {
return toast({
status: 'error',
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
});
}
setLoading(false);
}, [nodeId, onResetNode, pluginId, setLoading, t, toast]);
const Render = useMemo(() => {
const menuList = [
@@ -236,61 +287,6 @@ const MenuRender = React.memo(function MenuRender({
onClick: () => openDebugNode({ entryNodeId: nodeId })
}
]),
...(flowNodeType === FlowNodeTypeEnum.pluginModule
? [
{
icon: 'common/refreshLight',
label: t('plugin.Synchronous version'),
variant: 'whiteBase',
onClick: () => {
if (!pluginId) return;
onOpenConfirmSync(async () => {
try {
setLoading(true);
const pluginModule = await getPreviewPluginModule(pluginId);
onResetNode({
id: nodeId,
module: pluginModule
});
} catch (e) {
return toast({
status: 'error',
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
});
}
setLoading(false);
})();
}
}
]
: []),
...(menuForbid?.rename
? []
: [
{
icon: 'edit',
label: t('common.Rename'),
variant: 'whiteBase',
onClick: () =>
onOpenCustomTitleModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app.modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
nodeId,
type: 'attr',
key: 'name',
value: e
});
}
})
}
]),
...(menuForbid?.copy
? []
: [
@@ -301,6 +297,17 @@ const MenuRender = React.memo(function MenuRender({
onClick: () => onCopyNode(nodeId)
}
]),
...(flowNodeType === FlowNodeTypeEnum.pluginModule
? [
{
icon: 'common/refreshLight',
label: t('plugin.Synchronous version'),
variant: 'whiteBase',
onClick: onOpenConfirmSync(onclickSyncVersion)
}
]
: []),
...(menuForbid?.delete
? []
: [
@@ -342,7 +349,6 @@ const MenuRender = React.memo(function MenuRender({
</Box>
))}
</Box>
<EditTitleModal maxLength={20} />
<ConfirmSyncModal />
<ConfirmDeleteModal />
<DebugInputModal />
@@ -352,26 +358,18 @@ const MenuRender = React.memo(function MenuRender({
ConfirmDeleteModal,
ConfirmSyncModal,
DebugInputModal,
EditTitleModal,
flowNodeType,
menuForbid?.copy,
menuForbid?.debug,
menuForbid?.delete,
menuForbid?.rename,
name,
nodeId,
onChangeNode,
onCopyNode,
onDelNode,
onOpenConfirmDeleteNode,
onOpenConfirmSync,
onOpenCustomTitleModal,
onResetNode,
onclickSyncVersion,
openDebugNode,
pluginId,
setLoading,
t,
toast
t
]);
return Render;
@@ -388,7 +386,7 @@ const NodeIntro = React.memo(function NodeIntro({
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const moduleIsTool = useMemo(() => {
const NodeIsTool = useMemo(() => {
const { isTool } = splitToolInputs([], nodeId);
return isTool;
}, [nodeId, splitToolInputs]);
@@ -407,7 +405,7 @@ const NodeIntro = React.memo(function NodeIntro({
<Box fontSize={'xs'} color={'myGray.600'} flex={'1 0 0'}>
{t(intro)}
</Box>
{moduleIsTool && (
{NodeIsTool && (
<Button
size={'xs'}
variant={'whiteBase'}
@@ -432,7 +430,7 @@ const NodeIntro = React.memo(function NodeIntro({
<EditIntroModal maxLength={500} />
</>
);
}, [EditIntroModal, intro, moduleIsTool, nodeId, onChangeNode, onOpenIntroModal, t]);
}, [EditIntroModal, intro, NodeIsTool, nodeId, onChangeNode, onOpenIntroModal, t]);
return Render;
});
@@ -526,7 +524,8 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
top={0}
zIndex={10}
w={'420px'}
maxH={'540px'}
maxH={'100%'}
minH={'300px'}
overflowY={'auto'}
border={'base'}
>

View File

@@ -90,7 +90,7 @@ export default React.memo(Reference);
export const useReference = ({
nodeId,
valueType,
valueType = WorkflowIOValueTypeEnum.any,
value
}: {
nodeId: string;

View File

@@ -36,7 +36,6 @@ import { createContext } from 'use-context-selector';
import { defaultRunningStatus } from './constants';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@@ -56,7 +55,7 @@ type WorkflowContextType = {
hoverNodeId?: string;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onUpdateNodeError: (node: string, isError: Boolean) => void;
onResetNode: (e: { id: string; module: FlowNodeTemplateType }) => void;
onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
// edges
@@ -160,7 +159,7 @@ export const WorkflowContext = createContext<WorkflowContextType>({
onEdgesChange: function (changes: EdgeChange[]): void {
throw new Error('Function not implemented.');
},
onResetNode: function (e: { id: string; module: FlowNodeTemplateType }): void {
onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void {
throw new Error('Function not implemented.');
},
onDelEdge: function (e: {
@@ -256,7 +255,11 @@ const WorkflowContextProvider = ({
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const [hoverNodeId, setHoverNodeId] = useState<string>();
const nodeList = useCreation(() => nodes.map((node) => node.data), [nodes]);
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
const nodeList = useMemo(
() => JSON.parse(nodeListString) as FlowNodeItemType[],
[nodeListString]
);
const hasToolNode = useMemo(() => {
return !!nodes.find((node) => node.data.flowNodeType === FlowNodeTypeEnum.tools);
@@ -276,31 +279,30 @@ const WorkflowContextProvider = ({
});
// reset a node data. delete edge and replace it
const onResetNode = useMemoizedFn(
({ id, module }: { id: string; module: FlowNodeTemplateType }) => {
setNodes((state) =>
state.map((node) => {
if (node.id === id) {
// delete edge
node.data.inputs.forEach((item) => {
onDelEdge({ nodeId: id, targetHandle: item.key });
});
node.data.outputs.forEach((item) => {
onDelEdge({ nodeId: id, sourceHandle: item.key });
});
return {
const onResetNode = useMemoizedFn(({ id, node }: { id: string; node: FlowNodeTemplateType }) => {
setNodes((state) =>
state.map((item) => {
if (item.id === id) {
return {
...item,
data: {
...item.data,
...node,
data: {
...node.data,
...module
}
};
}
return node;
})
);
}
);
inputs: node.inputs.map((input) => {
const value =
item.data.inputs.find((i) => i.key === input.key)?.value ?? input.value;
return {
...input,
value
};
})
}
};
}
return item;
})
);
});
const onChangeNode = useMemoizedFn((props: FlowNodeChangeProps) => {
const { nodeId, type } = props;
@@ -410,7 +412,7 @@ const WorkflowContextProvider = ({
});
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = useMemoizedFn((inputs: FlowNodeInputItemType[], nodeId: string) => {
const splitToolInputs = (inputs: FlowNodeInputItemType[], nodeId: string) => {
const isTool = !!edges.find(
(edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId
);
@@ -423,12 +425,11 @@ const WorkflowContextProvider = ({
return !item.toolDescription;
})
};
});
};
const initData = useMemoizedFn(
async (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })));
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })));
}
);

View File

@@ -63,7 +63,10 @@ export async function getInitConfig() {
initSystemConfig(),
// getSimpleModeTemplates(),
getSystemVersion(),
getSystemPlugin()
getSystemPlugin(),
// abandon
getSystemPluginV1()
]);
console.log({
@@ -164,3 +167,29 @@ function getSystemPlugin() {
global.communityPlugins = fileTemplates;
}
function getSystemPluginV1() {
if (global.communityPluginsV1 && global.communityPluginsV1.length > 0) return;
const basePath =
process.env.NODE_ENV === 'development'
? 'data/pluginTemplates/v1'
: '/app/data/pluginTemplates/v1';
// read data/pluginTemplates directory, get all json file
const files = readdirSync(basePath);
// filter json file
const filterFiles = files.filter((item) => item.endsWith('.json'));
// read json file
const fileTemplates: (PluginTemplateType & { weight: number })[] = filterFiles.map((filename) => {
const content = readFileSync(`${basePath}/${filename}`, 'utf-8');
return {
...JSON.parse(content),
id: `${PluginSourceEnum.community}-${filename.replace('.json', '')}`,
source: PluginSourceEnum.community
};
});
fileTemplates.sort((a, b) => b.weight - a.weight);
global.communityPluginsV1 = fileTemplates;
}

View File

@@ -5,7 +5,11 @@ import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/cons
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { ChatItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import type {
ChatItemType,
ChatItemValueItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
@@ -13,10 +17,11 @@ import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/a
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
export type Props = {
history: ChatItemType[];
prompt: ChatItemValueItemType[];
prompt: UserChatItemValueItemType[];
nodes: RuntimeNodeItemType[];
edges: RuntimeEdgeItemType[];
variables: Record<string, any>;
@@ -33,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.end();
});
const {
let {
nodes = [],
edges = [],
history = [],
@@ -66,10 +71,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
const { text, files } = chatValue2RuntimePrompt(prompt);
/* start process */
const { flowResponses, flowUsages } = await dispatchWorkFlow({
const { flowResponses, flowUsages, newVariables } = await dispatchWorkFlow({
res,
mode: 'test',
teamId,
@@ -78,11 +81,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
appId,
runtimeNodes: nodes,
runtimeEdges: edges,
variables: {
...variables,
userChatInput: text
},
inputFiles: files,
variables,
query: removeEmptyUserInput(prompt),
histories: history,
stream: true,
detail: true,
@@ -99,6 +99,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
event: SseResponseEventEnum.flowResponses,
data: JSON.stringify(flowResponses)
});
responseWrite({
res,
event: SseResponseEventEnum.updateVariables,
data: JSON.stringify(newVariables)
});
res.end();
pushChatUsage({

View File

@@ -53,11 +53,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
appId,
runtimeNodes: nodes,
runtimeEdges: edges,
variables: {
...variables,
userChatInput: ''
},
inputFiles: [],
variables,
query: [],
histories: [],
stream: false,
detail: true,

View File

@@ -0,0 +1,72 @@
import type { NextApiRequest, NextApiResponse } from 'next';
// @ts-ignore
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
type Props = HttpBodyType<{
input: string;
rule?: string;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { input, rule = '' } = req.body as Props;
await authRequestFromLocal({ req });
const result = (() => {
if (typeof input === 'string') {
const defaultReg: any[] = [
'',
undefined,
'undefined',
null,
'null',
false,
'false',
0,
'0',
'none'
];
const customReg = rule.split('\n');
defaultReg.push(...customReg);
return !defaultReg.find((item) => {
const reg = typeof item === 'string' ? stringToRegex(item) : null;
if (reg) {
return reg.test(input);
}
return input === item;
});
}
return !!input;
})();
res.json({
...(result
? {
true: true
}
: {
false: false
})
});
} catch (err) {
console.log(err);
res.status(500).send(getErrText(err));
}
}
function stringToRegex(str: string) {
const regexFormat = /^\/(.+)\/([gimuy]*)$/;
const match = str.match(regexFormat);
if (match) {
const [, pattern, flags] = match;
return new RegExp(pattern, flags);
} else {
return null;
}
}

View File

@@ -1,25 +1,36 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/workflow/api.d';
//@ts-ignore
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { addCustomFeedbacks } from '@fastgpt/service/core/chat/controller';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
type Props = HttpBodyType<{
appId: string;
chatId?: string;
responseChatItemId?: string;
defaultFeedback: string;
customFeedback: string;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
customFeedback,
system_addInputParam: { appId, chatId, responseChatItemId: chatItemId }
appId,
chatId,
responseChatItemId: chatItemId,
defaultFeedback,
customFeedback
} = req.body as Props;
await authRequestFromLocal({ req });
if (!customFeedback) {
return res.json({});
const feedback = customFeedback || defaultFeedback;
if (!feedback) {
return res.json({
response: ''
});
}
// wait the chat finish
@@ -28,17 +39,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
appId,
chatId,
chatItemId,
feedbacks: [customFeedback]
feedbacks: [feedback]
});
}, 60000);
if (!chatId || !chatItemId) {
return res.json({
[NodeOutputKeyEnum.answerText]: `\\n\\n**自动反馈调试**: "${customFeedback}"\\n\\n`
response: `\\n\\n**自动反馈调试**: ${feedback}\\n\\n`
});
}
return res.json({});
return res.json({
response: ''
});
} catch (err) {
console.log(err);
res.status(500).send(getErrText(err));

View File

@@ -0,0 +1,46 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/workflow/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { addCustomFeedbacks } from '@fastgpt/service/core/chat/controller';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
type Props = HttpBodyType<{
customFeedback: string;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
customFeedback,
[NodeInputKeyEnum.addInputParam]: { appId, chatId, responseChatItemId: chatItemId }
} = req.body as Props;
await authRequestFromLocal({ req });
if (!customFeedback) {
return res.json({});
}
// wait the chat finish
setTimeout(() => {
addCustomFeedbacks({
appId,
chatId,
chatItemId,
feedbacks: [customFeedback]
});
}, 60000);
if (!chatId || !chatItemId) {
return res.json({
[NodeOutputKeyEnum.answerText]: `\\n\\n**自动反馈调试**: "${customFeedback}"\\n\\n`
});
}
return res.json({});
} catch (err) {
console.log(err);
res.status(500).send(getErrText(err));
}
}

View File

@@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/workflow/api.d';
//@ts-ignore
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
type Props = HttpBodyType<{
text: string;
@@ -12,7 +12,10 @@ type Props = HttpBodyType<{
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { text, [NodeInputKeyEnum.addInputParam]: obj } = req.body as Props;
const {
text,
DYNAMIC_INPUT_KEY: { ...obj }
} = req.body as Props;
await authRequestFromLocal({ req });

View File

@@ -0,0 +1,42 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/workflow/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
type Props = HttpBodyType<{
text: string;
[key: string]: any;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { text, [NodeInputKeyEnum.addInputParam]: obj } = req.body as Props;
await authRequestFromLocal({ req });
// string all value
Object.keys(obj).forEach((key) => {
let val = obj[key];
if (typeof val === 'object') {
val = JSON.stringify(val);
} else if (typeof val === 'number') {
val = String(val);
} else if (typeof val === 'boolean') {
val = val ? 'true' : 'false';
}
obj[key] = val;
});
const textResult = replaceVariable(text, obj);
res.json({
text: textResult
});
} catch (err) {
console.log(err);
res.status(500).send(getErrText(err));
}
}

View File

@@ -24,7 +24,10 @@ import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/out
import requestIp from 'request-ip';
import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import {
filterPublicNodeResponseData,
removeEmptyUserInput
} from '@fastgpt/global/core/chat/utils';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { connectToDatabase } from '@/service/mongo';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
@@ -42,7 +45,6 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
import { dispatchWorkFlowV1 } from '@fastgpt/service/core/workflow/dispatchV1';
import { setEntryEntries } from '@fastgpt/service/core/workflow/dispatchV1/utils';
import { NextAPI } from '@/service/middle/entry';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
type FastGptWebChatProps = {
@@ -179,7 +181,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const responseChatItemId: string | undefined = messages[messages.length - 1].dataId;
/* start flow controller */
const { flowResponses, flowUsages, assistantResponses } = await (async () => {
const { flowResponses, flowUsages, assistantResponses, newVariables } = await (async () => {
if (app.version === 'v2') {
return dispatchWorkFlow({
res,
@@ -192,11 +194,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
responseChatItemId,
runtimeNodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
runtimeEdges: initWorkflowEdgeStatus(edges),
variables: {
...variables,
userChatInput: text
},
inputFiles: files,
variables,
query: removeEmptyUserInput(question.value),
histories: concatHistories,
stream,
detail,
@@ -247,7 +246,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
appId: app._id,
teamId,
tmbId: tmbId,
variables,
variables: newVariables,
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
shareId,
outLinkUid: outLinkUserId,
@@ -288,6 +287,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
event: detail ? SseResponseEventEnum.answer : undefined,
data: '[DONE]'
});
responseWrite({
res,
event: SseResponseEventEnum.updateVariables,
data: JSON.stringify(newVariables)
});
if (responseDetail && detail) {
responseWrite({

View File

@@ -75,6 +75,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
WorkflowContext,
(v) => v.setIsShowVersionHistories
);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
@@ -93,35 +94,40 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
}
}, [edges, onUpdateNodeError, t, toast]);
const onclickSave = useCallback(async () => {
if (isShowVersionHistories) return;
const { nodes } = await getWorkflowStore();
const onclickSave = useCallback(
async (forbid?: boolean) => {
// version preview / debug mode, not save
if (isShowVersionHistories || forbid) return;
if (nodes.length === 0) return null;
setIsSaving(true);
const { nodes } = await getWorkflowStore();
const storeWorkflow = flowNode2StoreNodes({ nodes, edges });
if (nodes.length === 0) return null;
setIsSaving(true);
try {
await updateAppDetail(app._id, {
...storeWorkflow,
type: AppTypeEnum.advanced,
//@ts-ignore
version: 'v2'
});
const storeWorkflow = flowNode2StoreNodes({ nodes, edges });
setSaveLabel(
t('core.app.Auto Save time', {
time: formatTime2HM()
})
);
// ChatTestRef.current?.resetChatTest();
} catch (error) {}
try {
await updateAppDetail(app._id, {
...storeWorkflow,
type: AppTypeEnum.advanced,
//@ts-ignore
version: 'v2'
});
setIsSaving(false);
setSaveLabel(
t('core.app.Auto Save time', {
time: formatTime2HM()
})
);
// ChatTestRef.current?.resetChatTest();
} catch (error) {}
return null;
}, [isShowVersionHistories, edges, updateAppDetail, app._id, t]);
setIsSaving(false);
return null;
},
[isShowVersionHistories, edges, updateAppDetail, app._id, t]
);
const onclickPublish = useCallback(async () => {
setIsSaving(true);
@@ -182,7 +188,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
useInterval(() => {
if (!app._id) return;
onclickSave();
onclickSave(!!workflowDebugData);
}, 20000);
const Render = useMemo(() => {
@@ -221,7 +227,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
display={'inline-block'}
borderRadius={'xs'}
cursor={'pointer'}
onClick={onclickSave}
onClick={() => onclickSave()}
color={'myGray.500'}
>
{saveLabel}
@@ -232,39 +238,43 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
<Box flex={1} />
<MyMenu
Button={
{!isShowVersionHistories && (
<>
<MyMenu
Button={
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
aria-label={''}
size={'sm'}
variant={'whitePrimary'}
/>
}
menuList={[
{
label: t('app.Import Configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: t('app.Export Configs'),
icon: 'export',
onClick: onExportWorkflow
}
]}
/>
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => setIsShowVersionHistories(true)}
/>
}
menuList={[
{
label: t('app.Import Configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: t('app.Export Configs'),
icon: 'export',
onClick: onExportWorkflow
}
]}
/>
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => setIsShowVersionHistories(true)}
/>
</>
)}
<Button
size={'sm'}

View File

@@ -28,7 +28,7 @@ const Render = ({ app, onClose }: Props) => {
useEffect(() => {
if (!isV2Workflow) return;
initData(JSON.parse(workflowStringData));
}, [isV2Workflow, initData, workflowStringData]);
}, [isV2Workflow, initData, app._id]);
useEffect(() => {
if (!isV2Workflow) {

View File

@@ -62,7 +62,7 @@ const ChatTest = ({
}
});
});
const history = chatList.slice(-historyMaxLen - 2, -2);
const history = chatList.slice(-(historyMaxLen * 2) - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({

View File

@@ -74,7 +74,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
const prompts = messages.slice(-2);
const completionChatId = chatId ? chatId : nanoid();
const { responseText, responseData } = await streamFetch({
const { responseText, responseData, newVariables } = await streamFetch({
data: {
messages: prompts,
variables,
@@ -123,7 +123,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
history: ChatBoxRef.current?.getChatHistories() || state.history
}));
return { responseText, responseData, isNewChat: forbidRefresh.current };
return { responseText, responseData, isNewChat: forbidRefresh.current, newVariables };
},
[appId, chatId, histories, pushHistory, router, setChatData, updateHistory]
);

View File

@@ -95,12 +95,12 @@ const OutLink = ({
'*'
);
const { responseText, responseData } = await streamFetch({
const { responseText, responseData, newVariables } = await streamFetch({
data: {
messages: prompts,
variables: {
...customVariables,
...variables
...variables,
...customVariables
},
shareId,
chatId: completionChatId,
@@ -169,7 +169,7 @@ const OutLink = ({
'*'
);
return { responseText, responseData, isNewChat: forbidRefresh.current };
return { responseText, responseData, isNewChat: forbidRefresh.current, newVariables };
},
[
chatId,

View File

@@ -79,7 +79,7 @@ const OutLink = () => {
const prompts = messages.slice(-2);
const completionChatId = chatId ? chatId : nanoid();
const { responseText, responseData } = await streamFetch({
const { responseText, responseData, newVariables } = await streamFetch({
data: {
messages: prompts,
variables: {
@@ -135,7 +135,7 @@ const OutLink = () => {
history: ChatBoxRef.current?.getChatHistories() || state.history
}));
return { responseText, responseData, isNewChat: forbidRefresh.current };
return { responseText, responseData, isNewChat: forbidRefresh.current, newVariables };
},
[appId, teamToken, chatId, histories, pushHistory, router, setChatData, teamId, updateHistory]
);

View File

@@ -31,7 +31,7 @@ export const defaultForm: EditFormType = {
modules: [
{
nodeId: nanoid(),
name: '定义插件输入',
name: '定义插件输入',
avatar: '/imgs/workflow/input.png',
flowNodeType: 'pluginInput',
showStatus: false,
@@ -44,7 +44,7 @@ export const defaultForm: EditFormType = {
},
{
nodeId: nanoid(),
name: '定义插件输出',
name: '定义插件输出',
avatar: '/imgs/workflow/output.png',
flowNodeType: 'pluginOutput',
showStatus: false,

View File

@@ -1,6 +1,7 @@
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { delay } from '@fastgpt/global/common/system/utils';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import {
getDefaultEntryNodeIds,
initWorkflowEdgeStatus,
@@ -34,9 +35,15 @@ export const getScheduleTriggerApp = async () => {
appId: String(app._id),
runtimeNodes: storeNodes2RuntimeNodes(app.modules, getDefaultEntryNodeIds(app.modules)),
runtimeEdges: initWorkflowEdgeStatus(app.edges),
variables: {
userChatInput: app.scheduledTriggerConfig?.defaultPrompt
},
variables: {},
query: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: app.scheduledTriggerConfig?.defaultPrompt
}
}
],
histories: [],
stream: false,
detail: false,

View File

@@ -42,7 +42,6 @@ export const pushChatUsage = ({
addLog.info(`finish completions`, {
source,
teamId,
tmbId,
totalPoints
});
return { totalPoints };

View File

@@ -69,6 +69,7 @@ export async function saveChat({
chat.title = title;
chat.updateTime = new Date();
chat.metadata = metadataUpdate;
chat.variables = variables || {};
await chat.save({ session });
} else {
await MongoChat.create(

View File

@@ -22,6 +22,7 @@ type StreamFetchProps = {
type StreamResponseType = {
responseText: string;
[DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[];
newVariables: Record<string, any>;
};
class FatalError extends Error {}
@@ -50,6 +51,7 @@ export const streamFetch = ({
)[] = [];
let errMsg: string | undefined;
let responseData: ChatHistoryItemResType[] = [];
let newVariables: Record<string, any> = {};
let finished = false;
const finish = () => {
@@ -57,6 +59,7 @@ export const streamFetch = ({
return failedFinish();
}
return resolve({
newVariables,
responseText,
responseData
});
@@ -198,6 +201,8 @@ export const streamFetch = ({
});
} else if (event === SseResponseEventEnum.flowResponses && Array.isArray(parseJson)) {
responseData = parseJson;
} else if (event === SseResponseEventEnum.updateVariables) {
newVariables = parseJson;
} else if (event === SseResponseEventEnum.error) {
if (parseJson.statusText === TeamErrEnum.aiPointsNotEnough) {
useSystemStore.getState().setIsNotSufficientModal(true);

View File

@@ -14,7 +14,7 @@ import { EmptyNode } from '@fastgpt/global/core/workflow/template/system/emptyNo
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { getGlobalVariableNode } from './adapt';
import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { VARIABLE_NODE_ID, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import {
@@ -24,6 +24,9 @@ import {
} from '@fastgpt/global/core/workflow/utils';
import { getSystemVariables } from '../app/utils';
import { TFunction } from 'next-i18next';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { VariableConditionEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
export const nodeTemplate2FlowNode = ({
template,
@@ -140,6 +143,26 @@ export const computedNodeInputReference = ({
return sourceNodes;
};
export const getReferenceDataValueType = ({
variable,
nodeList,
t
}: {
variable?: ReferenceValueProps;
nodeList: FlowNodeItemType[];
t: TFunction;
}) => {
if (!variable) return WorkflowIOValueTypeEnum.any;
const node = nodeList.find((node) => node.nodeId === variable[0]);
const systemVariables = getWorkflowGlobalVariables(nodeList, t);
if (!node) return systemVariables.find((item) => item.key === variable?.[1])?.valueType;
const output = node.outputs.find((item) => item.id === variable[1]);
if (!output) return WorkflowIOValueTypeEnum.any;
return output.valueType;
};
/* Connection rules */
export const checkWorkflowNodeAndConnection = ({
@@ -167,6 +190,29 @@ export const checkWorkflowNodeAndConnection = ({
continue;
}
if (data.flowNodeType === FlowNodeTypeEnum.ifElseNode) {
const ifElseList: IfElseListItemType[] = inputs.find(
(input) => input.key === NodeInputKeyEnum.ifElseList
)?.value;
if (
ifElseList.some((item) => {
return item.list.some((listItem) => {
return (
listItem.variable === undefined ||
listItem.condition === undefined ||
(listItem.value === undefined &&
listItem.condition !== VariableConditionEnum.isEmpty &&
listItem.condition !== VariableConditionEnum.isNotEmpty)
);
});
})
) {
return [data.nodeId];
} else {
continue;
}
}
// check node input
if (
inputs.some((input) => {
@@ -243,7 +289,10 @@ export const getWorkflowGlobalVariables = (
): EditorVariablePickerType[] => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes))?.variableModules || []
);
).map((item) => ({
...item,
valueType: WorkflowIOValueTypeEnum.any
}));
const systemVariables = getSystemVariables(t);