4.8.6 merge (#1943)

* Dataset collection forbid (#1885)

* perf: tool call support same id

* feat: collection forbid

* feat: collection forbid

* Inheritance Permission for apps (#1897)

* feat: app schema define

chore: references of authapp

* feat: authApp method inheritance

* feat: create and update api

* feat: update

* feat: inheritance Permission controller for app.

* feat: abstract version of inheritPermission

* feat: ancestorId for apps

* chore: update app

* fix: inheritPermission abstract version

* feat: update folder defaultPermission

* feat: app update api

* chore: inheritance frontend

* chore: app list api

* feat: update defaultPermission in app deatil

* feat: backend api finished

* feat: app inheritance permission fe

* fix: app update defaultpermission causes collaborator miss

* fix: ts error

* chore: adjust the codes

* chore: i18n

chore: i18n

* chore: fe adjust and i18n

* chore: adjust the code

* feat: resume api;
chore: rewrite update api and inheritPermission methods

* chore: something

* chore: fe code adjusting

* feat: frontend adjusting

* chore: fe code adjusting

* chore: adjusting the code

* perf: fe loading

* format

* Inheritance fix (#1908)

* fix: SlideCard

* fix: authapp did not return parent app for inheritance app

* fix: fe adjusting

* feat: fe adjusing

* perf: inherit per ux

* doc

* fix: ts errors (#1916)

* perf: inherit permission

* fix: permission inherit

* Workflow type (#1938)

* perf: workflow type

tmp workflow

perf: workflow type

feat: custom field config

* perf: dynamic input

* perf: node classify

* perf: node classify

* perf: node classify

* perf: node classify

* fix: workflow custom input

* feat: text editor and customFeedback move to basic nodes

* feat: community system plugin

* fix: ts

* feat: exprEval plugin

* perf: workflow type

* perf: plugin important

* fix: default templates

* perf: markdown hr css

* lock

* perf: fetch url

* perf: new plugin version

* fix: chat histories update

* fix: collection paths invalid

* perf: app card ui

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-07-04 17:42:09 +08:00
committed by GitHub
parent babf03c218
commit a9cdece341
303 changed files with 18883 additions and 13149 deletions

View File

@@ -1,32 +0,0 @@
## 插件类型
xxx.json 文件
```ts
type TemplateType =
| 'userGuide'
| 'systemInput'
| 'tools'
| 'textAnswer'
| 'functionCall'
| 'externalCall'
| 'other';
type pluginType = {
author: string; // 填写作者信息
templateType: FlowNodeTemplateType['templateType'];
name: string;
avatar: string;
intro: string;
showStatus?: boolean; // 是否需要展示组件运行状态
modules: []; //直接从高级编排导出配置复制过来;
};
```
## 额外代码怎么写?
参考 `TFSwitch``TextEditor`,通过 HTTP 模块将数据转到一个接口中实现。提交到社区的插件,务必将所有代码都放置在 FastGPT 仓库中,可以在 `projects/app/src/pages/api/plugins` 下新建一个与**插件文件名相同**的子目录进行接口编辑。
## 需要装包怎么办?
可以在 `packages/plugins` 下创建一个与**插件文件名相同**的子目录进行编写,可在 plugins 目录下安装相关依赖。然后在 FastGPT 主项目的接口中通过 `@fastgpt/plugins/xxx` 引入。

View File

@@ -8,227 +8,295 @@
"showStatus": false,
"isTool": false,
"weight": 0,
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [
{
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "",
"canEdit": true,
"value": "",
"editField": {
"key": true
"workflow": {
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 541.7107154264237,
"y": -165.05298493910115
},
"version": "481",
"inputs": [
{
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "自定义输入变量",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "",
"canEdit": true,
"value": "",
"customInputConfig": {
"selectValueTypeList": ["string"],
"showDescription": false,
"showDefaultValue": false
}
},
"dynamicParamDefaultValue": {
"inputType": "reference",
{
"key": "反馈内容",
"valueType": "string",
"required": true
"label": "反馈内容",
"renderTypeList": ["textarea", "reference"],
"description": "",
"canEdit": true,
"value": "",
"maxLength": ""
}
},
{
"key": "反馈内容",
"valueType": "string",
"label": "反馈内容",
"renderTypeList": ["textarea"],
"description": "",
"canEdit": true,
"value": "",
"editField": {
"key": true
},
"maxLength": "",
"dynamicParamDefaultValue": {
"inputType": "reference",
],
"outputs": [
{
"id": "ILc8GS7iU53M",
"key": "反馈内容",
"valueType": "string",
"label": "反馈内容",
"type": "static"
},
{
"id": "2LCxDnOSculb",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"type": "static"
}
]
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"version": "481",
"inputs": [
{
"key": "text",
"valueType": "string",
"label": "text",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"description": true,
"valueType": true
},
"value": ["CRT7oIEU8v2P", "pYKS0LB9gAr3"]
}
],
"outputs": []
},
{
"nodeId": "CRT7oIEU8v2P",
"name": "HTTP 请求",
"intro": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)",
"avatar": "/imgs/workflow/http.png",
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1070.8458389994719,
"y": -415.09022555407836
},
"version": "481",
"inputs": [
{
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"description": "core.module.input.description.HTTP Dynamic Input",
"editField": {
"key": true,
"valueType": true
},
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"],
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
}
},
{
"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,
"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 \"customInputs\": {{customInputs}},\r\n \"appId\": \"{{appId}}\",\r\n \"chatId\": \"{{chatId}}\",\r\n \"responseChatItemId\": \"{{responseChatItemId}}\"\r\n}",
"label": "",
"required": false
},
{
"key": "customFeedback",
"valueType": "string",
"label": "customFeedback",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"valueType": true
},
"value": ["lmpb9v2lo2lk", "ILc8GS7iU53M"]
},
{
"renderTypeList": ["reference"],
"valueType": "dynamic",
"canEdit": true,
"key": "customInputs",
"label": "customInputs",
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
},
"required": true,
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"]
}
}
],
"outputs": [
{
"id": "ILc8GS7iU53M",
"key": "反馈内容",
"valueType": "string",
"label": "反馈内容",
"type": "static"
},
{
"id": "2LCxDnOSculb",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"type": "static"
}
]
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"inputs": [
{
"key": "text",
"valueType": "string",
"label": "text",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"description": true,
"valueType": true
],
"outputs": [
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"customFieldConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
}
},
"value": ["CRT7oIEU8v2P", "pYKS0LB9gAr3"]
}
],
"outputs": []
},
{
"nodeId": "CRT7oIEU8v2P",
"name": "HTTP 请求",
"intro": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)",
"avatar": "/imgs/workflow/http.png",
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1070.8458389994719,
"y": -415.09022555407836
},
"inputs": [
{
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"description": "core.module.input.description.HTTP Dynamic Input",
"editField": {
"key": true,
"valueType": true
{
"id": "error",
"key": "error",
"label": "请求错误",
"description": "HTTP请求错误信息成功时返回空",
"valueType": "object",
"type": "static"
},
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"]
},
{
"key": "customFeedback",
"valueType": "string",
"label": "customFeedback",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"valueType": true
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"required": true,
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
},
"value": ["lmpb9v2lo2lk", "ILc8GS7iU53M"]
},
{
"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,
"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": [
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"editField": {
"key": true,
"valueType": true
{
"id": "pYKS0LB9gAr3",
"type": "dynamic",
"key": "text",
"valueType": "string",
"label": "text"
}
},
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
},
{
"id": "pYKS0LB9gAr3",
"type": "dynamic",
"key": "text",
"valueType": "string",
"label": "text"
}
]
}
],
"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"
}
]
]
}
],
"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

@@ -8,125 +8,127 @@
"showStatus": false,
"isTool": true,
"weight": 10,
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [],
"outputs": []
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"inputs": [
{
"key": "time",
"valueType": "string",
"label": "time",
"renderTypeList": ["reference"],
"required": false,
"description": "",
"canEdit": true,
"editField": {
"key": true,
"description": true,
"valueType": true
},
"value": ["WNUvWwYBUfEr", "HdIl1GWi0tnc"]
}
],
"outputs": []
},
{
"nodeId": "WNUvWwYBUfEr",
"name": "文本加工",
"intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。",
"flowNodeType": "pluginModule",
"showStatus": false,
"position": {
"x": 1030.0794269310472,
"y": -350.61013802286885
},
"inputs": [
{
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "",
"canEdit": false,
"value": "",
"editField": {
"key": true
},
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
"workflow": {
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
},
{
"key": "文本",
"valueType": "string",
"label": "文本",
"renderTypeList": ["textarea"],
"required": true,
"description": "",
"canEdit": false,
"value": "{{cTime}}",
"editField": {
"key": true
},
"maxLength": "",
"dynamicParamDefaultValue": {
"inputType": "reference",
"inputs": [],
"outputs": []
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"inputs": [
{
"key": "time",
"valueType": "string",
"required": true
"label": "time",
"renderTypeList": ["reference"],
"required": false,
"description": "",
"canEdit": true,
"editField": {
"key": true,
"description": true,
"valueType": true
},
"value": ["WNUvWwYBUfEr", "HdIl1GWi0tnc"]
}
}
],
"outputs": [
{
"id": "HdIl1GWi0tnc",
"key": "text",
"valueType": "string",
"label": "text",
"type": "static",
"canEdit": false
}
],
"pluginId": "community-textEditor"
}
],
"edges": [
{
"source": "lmpb9v2lo2lk",
"target": "WNUvWwYBUfEr",
"sourceHandle": "lmpb9v2lo2lk-source-right",
"targetHandle": "WNUvWwYBUfEr-target-left"
},
{
"source": "WNUvWwYBUfEr",
"target": "i7uow4wj2wdp",
"sourceHandle": "WNUvWwYBUfEr-source-right",
"targetHandle": "i7uow4wj2wdp-target-left"
}
]
],
"outputs": []
},
{
"nodeId": "WNUvWwYBUfEr",
"name": "文本加工",
"intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。",
"flowNodeType": "pluginModule",
"showStatus": false,
"position": {
"x": 1030.0794269310472,
"y": -350.61013802286885
},
"inputs": [
{
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "",
"canEdit": false,
"value": "",
"editField": {
"key": true
},
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
},
{
"key": "文本",
"valueType": "string",
"label": "文本",
"renderTypeList": ["textarea"],
"required": true,
"description": "",
"canEdit": false,
"value": "{{cTime}}",
"editField": {
"key": true
},
"maxLength": "",
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
}
],
"outputs": [
{
"id": "HdIl1GWi0tnc",
"key": "text",
"valueType": "string",
"label": "text",
"type": "static",
"canEdit": false
}
],
"pluginId": "community-textEditor"
}
],
"edges": [
{
"source": "lmpb9v2lo2lk",
"target": "WNUvWwYBUfEr",
"sourceHandle": "lmpb9v2lo2lk-source-right",
"targetHandle": "WNUvWwYBUfEr-target-left"
},
{
"source": "WNUvWwYBUfEr",
"target": "i7uow4wj2wdp",
"sourceHandle": "WNUvWwYBUfEr-source-right",
"targetHandle": "i7uow4wj2wdp-target-left"
}
]
}
}

View File

@@ -1,244 +1,314 @@
{
"author": "FastGPT Team",
"version": "481",
"templateType": "tools",
"name": "文本加工",
"avatar": "/imgs/workflow/textEditor.svg",
"intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。",
"showStatus": false,
"isTool": false,
"weight": 100,
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [
{
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "",
"canEdit": true,
"value": "",
"editField": {
"key": true
"isTool": false,
"templateType": "tools",
"workflow": {
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 449.3089648771014,
"y": -139.7660480324482
},
"version": "481",
"inputs": [
{
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "自定义输入变量",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "",
"canEdit": true,
"value": "",
"customInputConfig": {
"selectValueTypeList": ["string"],
"showDescription": false,
"showDefaultValue": false
}
},
"dynamicParamDefaultValue": {
"inputType": "reference",
{
"key": "文本",
"valueType": "string",
"required": true
"label": "文本",
"renderTypeList": ["textarea"],
"required": true,
"description": "",
"canEdit": true,
"value": "",
"maxLength": ""
}
},
{
"key": "文本",
"valueType": "string",
"label": "文本",
"renderTypeList": ["textarea"],
"required": true,
"description": "",
"canEdit": true,
"value": "",
"editField": {
"key": true
},
"maxLength": "",
"dynamicParamDefaultValue": {
"inputType": "reference",
],
"outputs": [
{
"id": "ILc8GS7iU53M",
"key": "文本",
"valueType": "string",
"label": "文本",
"type": "static"
},
{
"id": "2LCxDnOSculb",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "system_addInputParam",
"type": "static"
}
]
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"version": "481",
"inputs": [
{
"key": "text",
"valueType": "string",
"label": "text",
"renderTypeList": ["reference"],
"required": false,
"description": "",
"canEdit": true,
"editField": {
"key": true,
"description": true,
"valueType": true
},
"value": ["CRT7oIEU8v2P", "vooswj3VxKW8"]
}
],
"outputs": [
{
"id": "HdIl1GWi0tnc",
"key": "text",
"valueType": "string",
"label": "text",
"type": "static"
}
]
},
{
"nodeId": "CRT7oIEU8v2P",
"name": "HTTP 请求",
"intro": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)",
"avatar": "/imgs/workflow/http.png",
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1070.8458389994719,
"y": -415.09022555407836
},
"version": "486",
"inputs": [
{
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"description": "core.module.input.description.HTTP Dynamic Input",
"editField": {
"key": true,
"valueType": true
},
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"],
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
}
},
{
"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,
"value": "/api/plugins/textEditor/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 \"text\":\"{{text}}\",\r\n \"customInputs\": {{customInputs}}\r\n}",
"label": "",
"required": false
},
{
"key": "text",
"valueType": "string",
"label": "text",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"valueType": true
},
"value": ["lmpb9v2lo2lk", "ILc8GS7iU53M"]
},
{
"renderTypeList": ["reference"],
"valueType": "dynamic",
"canEdit": true,
"key": "customInputs",
"label": "customInputs",
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
},
"required": true,
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"]
}
}
],
"outputs": [
{
"id": "ILc8GS7iU53M",
"key": "文本",
"valueType": "string",
"label": "文本",
"type": "static"
},
{
"id": "2LCxDnOSculb",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"type": "static"
}
]
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"inputs": [
{
"key": "text",
"valueType": "string",
"label": "text",
"renderTypeList": ["reference"],
"required": false,
"description": "",
"canEdit": true,
"editField": {
"key": true,
"description": true,
"valueType": true
],
"outputs": [
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"customFieldConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
}
},
"value": ["CRT7oIEU8v2P", "vooswj3VxKW8"]
}
],
"outputs": [
{
"id": "HdIl1GWi0tnc",
"key": "text",
"valueType": "string",
"label": "text",
"type": "static"
}
]
},
{
"nodeId": "CRT7oIEU8v2P",
"name": "HTTP 请求",
"intro": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)",
"avatar": "/imgs/workflow/http.png",
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1070.8458389994719,
"y": -415.09022555407836
},
"inputs": [
{
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"description": "core.module.input.description.HTTP Dynamic Input",
"editField": {
"key": true,
"valueType": true
{
"id": "error",
"key": "error",
"label": "请求错误",
"description": "HTTP请求错误信息成功时返回空",
"valueType": "object",
"type": "static"
},
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"]
},
{
"key": "text",
"valueType": "string",
"label": "text",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"valueType": true
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"required": true,
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
},
"value": ["lmpb9v2lo2lk", "ILc8GS7iU53M"]
},
{
"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,
"value": "/api/plugins/textEditor/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 \"text\":\"{{text}}\",\r\n \"system_addInputParam\": {{system_addInputParam}}\r\n}",
"label": "",
"required": false
}
],
"outputs": [
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"editField": {
"key": true,
"valueType": true
{
"id": "vooswj3VxKW8",
"type": "dynamic",
"key": "text",
"valueType": "string",
"label": "text"
}
},
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
},
{
"id": "vooswj3VxKW8",
"type": "dynamic",
"key": "text",
"valueType": "string",
"label": "text"
}
]
}
],
"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"
}
]
]
}
],
"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

@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.8.5",
"version": "4.8.6",
"private": false,
"scripts": {
"dev": "next dev",

View File

@@ -1,14 +1,10 @@
### FastGPT V4.8.5
### FastGPT V4.8.6
1. 新增 - 应用使用新权限系统。
2. 新增 - 应用支持文件夹。
3. 优化 - 文本分割增加连续换行、制表符清除,避免大文本性能问题。
4. 重要修复 - 修复系统插件运行池数据污染问题,由于从内存获取,会导致全局污染。
5. 修复 - Debug 模式下,相同 source 和 target 内容,导致连线显示异常
6. 修复 - 定时执行初始化错误。
7. 修复 - 应用调用传参异常。
8. 修复 - ctrl + cv 复杂节点时nodeId错误。
9. 调整组件库全局theme。
1. 新增 - 知识库支持单个集合禁用功能
2. 新增 - 文件夹权限继承
3. 新增 - 网页抓取和数学计算器系统插件
4. 新增 - 移动文本加工和自定义反馈到基础节点中
5. 修复 - 工作流中团队插件加载异常
------

View File

@@ -0,0 +1 @@
<svg t="1719897603573" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4511" width="200" height="200"><path d="M717.312 476.16c-7.68-10.24-14.848-20.48-22.528-30.208-36.352-48.64-72.192-97.792-108.544-146.432-10.24-13.824-14.336-28.16-9.728-45.056 9.216-31.744 50.176-41.984 73.728-17.92 15.872 16.384 29.184 34.816 43.52 52.736 40.96 50.688 81.92 101.376 122.368 152.064 1.024 1.024 1.024 2.56 2.048 4.608 4.096-4.608 1.536-6.656 0.512-8.704-20.992-43.008-41.984-86.016-62.976-128.512-7.68-15.872-8.704-31.232 0-46.592 17.408-29.696 61.44-28.16 79.872 3.072 31.232 54.272 61.952 109.056 93.184 163.84 20.48 35.84 39.936 71.68 60.928 107.008 49.152 81.408 36.352 200.192-53.248 267.264-49.152 36.864-103.936 56.832-165.376 54.272-45.056-2.048-89.088-12.288-133.12-23.04-33.28-8.192-65.536-18.432-96.256-33.792-5.12-2.56-10.24-5.632-14.848-8.704-18.944-12.8-24.576-25.6-19.968-45.568 4.608-20.48 19.968-35.328 39.936-38.4 8.704-1.536 16.384-0.512 24.576 2.56 17.92 6.144 35.84 12.288 54.272 18.944 5.632 2.048 11.264 2.048 16.384-1.536 9.216-5.632 10.752-15.36 3.584-23.552-5.632-6.656-12.288-12.288-19.456-17.408-53.76-40.448-104.96-84.48-157.696-126.464-22.528-17.92-44.544-35.84-66.56-54.272-19.456-15.872-24.064-43.52-10.24-62.976 13.824-19.456 40.96-24.576 62.464-10.24 36.352 24.064 72.704 48.64 109.056 72.704 23.04 15.36 45.568 30.72 68.608 46.08 1.536 1.024 3.584 2.048 5.12 3.584 2.048 1.024 4.096 3.072 6.144 1.024 2.56-2.56-0.512-4.096-2.048-5.632-10.24-10.752-20.992-22.016-31.232-32.768-54.784-56.32-109.056-112.64-163.84-168.96-17.92-18.944-18.432-46.08-1.024-64 17.92-17.92 45.056-18.432 64.512 0 36.864 34.816 73.728 69.12 111.104 103.936 31.232 29.696 62.976 58.88 94.208 88.576 1.536-0.512 2.048-1.024 2.56-1.536z" fill="#4D81B7" p-id="4512"></path><path d="M471.04 82.944h384c48.64 0 84.48 35.84 84.48 84.48 0 52.224-0.512 104.96 0 157.184 0 10.24-6.144 7.68-11.776 8.192-6.656 0.512-9.728-0.512-9.728-8.704 0.512-52.224 0-104.448 0-156.672 0-37.376-27.136-64.512-64.512-64.512h-767.488c-37.376 0-65.024 27.136-65.024 64.512v587.264c0 37.888 27.136 64.512 64.512 64.512h371.2c3.584 0 8.704-2.56 9.728 2.56 1.536 5.632 1.536 11.264-0.512 16.896 0 1.024-2.56 1.536-3.584 1.536h-374.784c-51.712 0-87.552-35.84-87.552-87.04V167.424c0-49.664 36.352-86.016 86.016-86.016 129.024 1.536 257.024 1.536 385.024 1.536z" fill="#808080" p-id="4513"></path><path d="M345.088 438.272v193.024c0 24.576-22.528 40.96-44.032 31.744-14.848-6.656-19.968-19.968-19.968-35.328v-154.624-219.648c0-8.192 1.024-16.896 5.12-24.064 7.168-12.8 20.992-18.432 35.84-14.848 13.824 3.584 23.552 16.384 23.552 31.744v173.056c-0.512 5.632-0.512 12.288-0.512 18.944zM141.824 506.368v-126.976c0-20.992 16.384-31.744 31.744-32.256 16.896-0.512 30.72 14.336 30.72 31.744v253.952c0 18.432-13.824 32.256-31.232 32.256-18.432 0-31.232-13.312-31.232-32.256v-126.464z" fill="#CFE5F6" p-id="4514"></path><path d="M471.04 947.2h-265.728c-3.072 0-8.192 2.048-8.704-2.56-1.024-5.632-2.048-11.776 0.512-16.896 1.024-2.56 5.12-1.024 8.192-1.024h355.84c58.368 0 116.224 0.512 174.592 0 9.728 0 9.216 4.096 9.728 11.264 0.512 8.192-2.048 10.24-10.24 10.24-88.064-1.024-176.128-1.024-264.192-1.024z" fill="#808080" p-id="4515"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1 @@
<svg t="1719901713739" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4531" width="200" height="200"><path d="M322.953846 207.425641h525.128205c28.882051 0 52.512821 23.630769 52.512821 52.512821v105.025641c0 28.882051-23.630769 52.512821-52.512821 52.51282h-525.128205c-28.882051 0-52.512821-23.630769-52.51282-52.51282v-105.025641c0-28.882051 23.630769-52.512821 52.51282-52.512821z" fill="#20DAB4" p-id="4532"></path><path d="M827.076923 1024H196.923077c-73.517949 0-131.282051-57.764103-131.282051-131.282051V131.282051c0-73.517949 57.764103-131.282051 131.282051-131.282051h630.153846c73.517949 0 131.282051 57.764103 131.282051 131.282051v761.435898c0 73.517949-60.389744 131.282051-131.282051 131.282051zM196.923077 52.512821C152.287179 52.512821 118.153846 86.646154 118.153846 131.282051v761.435898c0 44.635897 34.133333 78.769231 78.769231 78.76923h630.153846c44.635897 0 78.769231-34.133333 78.769231-78.76923V131.282051c0-44.635897-34.133333-78.769231-78.769231-78.76923H196.923077z" fill="#106D5A" p-id="4533"></path><path d="M270.441026 414.851282c15.753846 0 26.25641 10.502564 26.25641 26.25641s-10.502564 26.25641-26.25641 26.256411-26.25641-10.502564-26.256411-26.256411 10.502564-26.25641 26.256411-26.25641m0-52.51282c-44.635897 0-78.769231 34.133333-78.769231 78.76923s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.76923-78.769231-36.758974-78.769231-78.76923-78.76923zM506.748718 414.851282c15.753846 0 26.25641 10.502564 26.25641 26.25641s-10.502564 26.25641-26.25641 26.256411-26.25641-10.502564-26.25641-26.256411 10.502564-26.25641 26.25641-26.25641m0-52.51282c-44.635897 0-78.769231 34.133333-78.769231 78.76923s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.769231-78.769231-36.758974-78.769231-78.769231-78.76923zM743.05641 414.851282c15.753846 0 26.25641 10.502564 26.256411 26.25641s-10.502564 26.25641-26.256411 26.256411-26.25641-10.502564-26.25641-26.256411 10.502564-26.25641 26.25641-26.25641m0-52.51282c-44.635897 0-78.769231 34.133333-78.769231 78.76923s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.769231-78.769231-36.758974-78.769231-78.769231-78.76923zM270.441026 598.646154c15.753846 0 26.25641 10.502564 26.25641 26.25641s-10.502564 26.25641-26.25641 26.25641-26.25641-10.502564-26.256411-26.25641 10.502564-26.25641 26.256411-26.25641m0-52.512821c-44.635897 0-78.769231 34.133333-78.769231 78.769231s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.76923-78.769231-36.758974-78.769231-78.76923-78.769231zM506.748718 598.646154c15.753846 0 26.25641 10.502564 26.25641 26.25641s-10.502564 26.25641-26.25641 26.25641-26.25641-10.502564-26.25641-26.25641 10.502564-26.25641 26.25641-26.25641m0-52.512821c-44.635897 0-78.769231 34.133333-78.769231 78.769231s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.769231-78.769231-36.758974-78.769231-78.769231-78.769231zM743.05641 598.646154c15.753846 0 26.25641 10.502564 26.256411 26.25641s-10.502564 26.25641-26.256411 26.25641-26.25641-10.502564-26.25641-26.25641 10.502564-26.25641 26.25641-26.25641m0-52.512821c-44.635897 0-78.769231 34.133333-78.769231 78.769231s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.769231-78.769231-36.758974-78.769231-78.769231-78.769231zM270.441026 782.441026c15.753846 0 26.25641 10.502564 26.25641 26.25641s-10.502564 26.25641-26.25641 26.25641-26.25641-10.502564-26.256411-26.25641c0-13.128205 10.502564-26.25641 26.256411-26.25641m0-52.512821c-44.635897 0-78.769231 34.133333-78.769231 78.769231s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.76923-78.769231c0-42.010256-36.758974-78.769231-78.76923-78.769231zM506.748718 782.441026c15.753846 0 26.25641 10.502564 26.25641 26.25641s-10.502564 26.25641-26.25641 26.25641-26.25641-10.502564-26.25641-26.25641c0-13.128205 10.502564-26.25641 26.25641-26.25641m0-52.512821c-44.635897 0-78.769231 34.133333-78.769231 78.769231s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.769231-78.769231c0-42.010256-36.758974-78.769231-78.769231-78.769231zM743.05641 782.441026c15.753846 0 26.25641 10.502564 26.256411 26.25641s-10.502564 26.25641-26.256411 26.25641-26.25641-10.502564-26.25641-26.25641c0-13.128205 10.502564-26.25641 26.25641-26.25641m0-52.512821c-44.635897 0-78.769231 34.133333-78.769231 78.769231s34.133333 78.769231 78.769231 78.769231 78.769231-34.133333 78.769231-78.769231c0-42.010256-36.758974-78.769231-78.769231-78.769231zM769.312821 181.169231v105.025641h-525.128206v-105.025641h525.128206m0-52.512821h-525.128206c-28.882051 0-52.512821 23.630769-52.51282 52.512821v105.025641c0 28.882051 23.630769 52.512821 52.51282 52.51282h525.128206c28.882051 0 52.512821-23.630769 52.51282-52.51282v-105.025641c0-28.882051-23.630769-52.512821-52.51282-52.512821z" fill="#106D5A" p-id="4534"></path></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -8,7 +8,7 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
<Image
fallbackSrc={LOGO_ICON}
fallbackStrategy={'onError'}
borderRadius={'md'}
// borderRadius={'md'}
objectFit={'contain'}
alt=""
w={w}

View File

@@ -15,7 +15,7 @@ const ChatAvatar = ({ src, type }: { src?: string; type: 'Human' | 'AI' }) => {
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
bg={type === 'Human' ? 'white' : 'primary.50'}
>
<Avatar src={src} w={'100%'} h={'100%'} />
<Avatar src={src} w={'100%'} h={'100%'} borderRadius={'sm'} />
</Box>
);
};

View File

@@ -158,7 +158,7 @@ ${JSON.stringify(questionGuides)}`;
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'1rem'} mr={2} />
<Avatar src={tool.toolAvatar} w={'1rem'} mr={2} />
<Box mr={1} fontSize={'sm'}>
{tool.toolName}
</Box>

View File

@@ -3,6 +3,7 @@ import { ChatItemType } from '@fastgpt/global/core/chat/type';
import { useCallback } from 'react';
import { htmlTemplate } from '@/web/core/chat/constants';
import { fileDownload } from '@/web/common/file/utils';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
export const useChatBox = () => {
const onExportChat = useCallback(
@@ -40,8 +41,30 @@ export const useChatBox = () => {
const map: Record<ExportChatType, () => void> = {
md: () => {
console.log(history);
fileDownload({
text: history.map((item) => item.value).join('\n\n'),
text: history
.map((item) => {
let result = `Role: ${item.obj}\n`;
const content = item.value.map((item) => {
if (item.type === ChatItemValueTypeEnum.text) {
return item.text?.content;
} else if (item.type === ChatItemValueTypeEnum.file) {
return `
![${item.file?.name}](${item.file?.url})
`;
} else if (item.type === ChatItemValueTypeEnum.tool) {
return `
\`\`\`Toll
${JSON.stringify(item.tools, null, 2)}
\`\`\`
`;
}
});
return result + content;
})
.join('\n\n-------\n\n'),
type: 'text/markdown',
filename: 'chat.md'
});

View File

@@ -10,7 +10,7 @@ const CommunityModal = ({ onClose }: { onClose: () => void }) => {
const { feConfigs } = useSystemStore();
return (
<MyModal isOpen={true} onClose={onClose} iconSrc="modal/concat" title={t('home.Community')}>
<MyModal isOpen={true} onClose={onClose} iconSrc="modal/concat" title={t('system.Concat us')}>
<ModalBody textAlign={'center'}>
<Markdown source={feConfigs?.concatMd || ''} />
</ModalBody>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Skeleton } from '@chakra-ui/react';
import MyPhotoView from '@fastgpt/web/components/common/Image/PhotoView';
import { useBoolean } from 'ahooks';
@@ -6,11 +6,13 @@ import { useBoolean } from 'ahooks';
const MdImage = ({ src }: { src?: string }) => {
const [isLoaded, { setTrue }] = useBoolean(false);
const [renderSrc, setRenderSrc] = useState(src);
return (
<Skeleton isLoaded={isLoaded}>
<MyPhotoView
borderRadius={'md'}
src={src}
src={renderSrc}
alt={''}
fallbackSrc={'/imgs/errImg.png'}
fallbackStrategy={'onError'}
@@ -21,10 +23,12 @@ const MdImage = ({ src }: { src?: string }) => {
minH={'120px'}
maxH={'500px'}
my={1}
mx={'auto'}
onLoad={() => {
setTrue();
}}
onError={() => {
setRenderSrc('/imgs/errImg.png');
setTrue();
}}
/>

View File

@@ -341,6 +341,7 @@
}
.markdown hr {
margin: 10px 0;
border-color: var(--chakra-colors-gray-300);
}
.markdown {
tab-size: 4;

View File

@@ -39,7 +39,7 @@ const FolderPath = (props: {
{concatPaths.map((item, i) => (
<Flex key={item.parentId || i} alignItems={'center'}>
<Box
fontSize={['sm', fontSize || 'sm']}
fontSize={['xs', fontSize || 'sm']}
py={0.5}
px={1.5}
borderRadius={'md'}

View File

@@ -1,4 +1,5 @@
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
@@ -13,6 +14,8 @@ import CollaboratorContextProvider, {
} from '../../support/permission/MemberManager/context';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
const FolderSlideCard = ({
refreshDeps,
@@ -24,7 +27,11 @@ const FolderSlideCard = ({
onDelete,
defaultPer,
managePer
managePer,
isInheritPermission,
resumeInheritPermission,
hasParent,
refetchResource
}: {
refreshDeps?: any[];
name: string;
@@ -40,9 +47,16 @@ const FolderSlideCard = ({
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
isInheritPermission?: boolean;
resumeInheritPermission?: () => Promise<void>;
hasParent?: boolean;
refetchResource?: () => Promise<any>;
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { commonT } = useI18n();
const { toast } = useToast();
const { ConfirmModal, openConfirm } = useConfirm({
type: 'delete',
@@ -118,6 +132,12 @@ const FolderSlideCard = ({
<Box>
<FormLabel>{t('support.permission.Permission')}</FormLabel>
{!isInheritPermission && (
<Box mt={2}>
<ResumeInherit onResume={() => resumeInheritPermission?.().then(refetchResource)} />
</Box>
)}
{managePer.permission.hasManagePer && (
<Box mt={5}>
<Box fontSize={'sm'} color={'myGray.500'}>
@@ -127,12 +147,20 @@ const FolderSlideCard = ({
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
onChange={defaultPer.onChange}
isInheritPermission={isInheritPermission}
onChange={(v) => defaultPer.onChange(v)}
hasParent={hasParent}
/>
</Box>
)}
<Box mt={6}>
<CollaboratorContextProvider {...managePer} refreshDeps={refreshDeps}>
<CollaboratorContextProvider
{...managePer}
refreshDeps={refreshDeps}
refetchResource={refetchResource}
isInheritPermission={isInheritPermission}
hasParent={hasParent}
>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>

View File

@@ -1,5 +1,6 @@
import React, { useState, DragEvent, useCallback } from 'react';
import type { BoxProps } from '@chakra-ui/react';
import { useBoolean } from 'ahooks';
export const useFolderDrag = ({
onDrop,
@@ -10,11 +11,13 @@ export const useFolderDrag = ({
}) => {
const [dragId, setDragId] = useState<string>();
const [targetId, setTargetId] = useState<string>();
const [isDropping, { setTrue, setFalse }] = useBoolean();
const getBoxProps = useCallback(
({ dataId, isFolder }: { dataId: string; isFolder: boolean }) => {
return {
draggable: true,
userSelect: 'none' as any,
'data-drag-id': isFolder ? dataId : undefined,
onDragStart: (e: DragEvent<HTMLDivElement>) => {
setDragId(dataId);
@@ -29,15 +32,19 @@ export const useFolderDrag = ({
e.preventDefault();
setTargetId(undefined);
},
onDrop: (e: DragEvent<HTMLDivElement>) => {
onDrop: async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setTrue();
if (targetId && dragId && targetId !== dragId) {
onDrop(dragId, targetId);
}
try {
if (targetId && dragId && targetId !== dragId) {
await onDrop(dragId, targetId);
}
} catch (error) {}
setTargetId(undefined);
setDragId(undefined);
setFalse();
},
...(activeStyles &&
targetId === dataId && {
@@ -45,10 +52,11 @@ export const useFolderDrag = ({
})
};
},
[activeStyles, dragId, onDrop, targetId]
[activeStyles, dragId, onDrop, setFalse, setTrue, targetId]
);
return {
getBoxProps
getBoxProps,
isDropping
};
};

View File

@@ -7,6 +7,9 @@ import { Box, Button, Flex, HStack, ModalBody } from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import DefaultPermissionList from '../DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '../ResumeInheritText';
export type ConfigPerModalProps = {
avatar?: string;
@@ -18,6 +21,10 @@ export type ConfigPerModalProps = {
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
isInheritPermission?: boolean;
resumeInheritPermission?: () => void;
hasParent?: boolean;
refetchResource?: () => void;
};
const ConfigPerModal = ({
@@ -25,71 +32,90 @@ const ConfigPerModal = ({
name,
defaultPer,
managePer,
onClose
isInheritPermission,
resumeInheritPermission,
hasParent,
onClose,
refetchResource
}: ConfigPerModalProps & {
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isOpen
iconSrc="/imgs/modal/key.svg"
onClose={onClose}
title={t('permission.Permission config')}
>
<ModalBody>
<HStack>
<Avatar src={avatar} w={'1.75rem'} />
<Box>{name}</Box>
</HStack>
<Box mt={6}>
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
onChange={defaultPer.onChange}
/>
</Box>
<Box mt={4}>
<CollaboratorContextProvider {...managePer}>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex
alignItems="center"
flexDirection="row"
justifyContent="space-between"
w="full"
>
<Box fontSize={'sm'}>{t('permission.Collaborator')}</Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common.Add')}
</Button>
<>
<MyModal
isOpen
iconSrc="/imgs/modal/key.svg"
onClose={onClose}
title={t('permission.Permission config')}
>
<ModalBody>
<HStack>
<Avatar src={avatar} w={'1.75rem'} />
<Box>{name}</Box>
</HStack>
{!isInheritPermission && (
<Box mt={3}>
<ResumeInherit onResume={resumeInheritPermission} />
</Box>
)}
<Box mt={5}>
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
isInheritPermission={isInheritPermission}
onChange={(v) => defaultPer.onChange(v)}
hasParent={hasParent}
/>
</Box>
<Box mt={4}>
<CollaboratorContextProvider
{...managePer}
refetchResource={refetchResource}
isInheritPermission={isInheritPermission}
hasParent={hasParent}
>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex
alignItems="center"
flexDirection="row"
justifyContent="space-between"
w="full"
>
<Box fontSize={'sm'}>{t('permission.Collaborator')}</Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common.Add')}
</Button>
</Flex>
</Flex>
</Flex>
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
</>
);
}}
</CollaboratorContextProvider>
</Box>
</ModalBody>
</MyModal>
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
</>
);
}}
</CollaboratorContextProvider>
</Box>
</ModalBody>
</MyModal>
</>
);
};

View File

@@ -1,10 +1,11 @@
import { Box, BoxProps } from '@chakra-ui/react';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useTranslation } from 'next-i18next';
import React from 'react';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
export enum defaultPermissionEnum {
private = 'private',
@@ -18,6 +19,8 @@ type Props = Omit<BoxProps, 'onChange'> & {
readPer?: PermissionValueType;
writePer?: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any> | any;
isInheritPermission?: boolean;
hasParent?: boolean;
};
const DefaultPermissionList = ({
@@ -26,28 +29,45 @@ const DefaultPermissionList = ({
readPer = ReadPermissionVal,
writePer = WritePermissionVal,
onChange,
isInheritPermission = false,
hasParent,
...styles
}: Props) => {
const { t } = useTranslation();
const { ConfirmModal, openConfirm } = useConfirm({});
const { commonT } = useI18n();
const defaultPermissionSelectList = [
{ label: '仅协作者访问', value: defaultPer },
{ label: '团队可访问', value: readPer },
{ label: '团队可编辑', value: writePer }
];
const { runAsync: onRequestChange, loading } = useRequest2(async (v: PermissionValueType) =>
const { runAsync: onRequestChange, loading } = useRequest2((v: PermissionValueType) =>
onChange(v)
);
return (
<Box {...styles}>
<MySelect
isLoading={loading}
list={defaultPermissionSelectList}
value={per}
onchange={onRequestChange}
/>
</Box>
<>
<Box {...styles}>
<MySelect
isLoading={loading}
list={defaultPermissionSelectList}
value={per}
onchange={(per) => {
if (isInheritPermission && hasParent) {
openConfirm(
() => onRequestChange(per),
undefined,
commonT('permission.Remove InheritPermission Confirm')
)();
} else {
return onRequestChange(per);
}
}}
/>
</Box>
<ConfirmModal />
</>
);
};

View File

@@ -1,4 +1,4 @@
import { BoxProps, useDisclosure } from '@chakra-ui/react';
import { useDisclosure } from '@chakra-ui/react';
import {
CollaboratorItemType,
UpdateClbPermissionProps
@@ -13,6 +13,8 @@ import dynamic from 'next/dynamic';
import MemberListCard, { MemberListCardProps } from './MemberListCard';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
@@ -66,12 +68,28 @@ const CollaboratorContextProvider = ({
permissionList,
onUpdateCollaborators,
onDelOneCollaborator,
children,
refetchResource,
refreshDeps = [],
children
isInheritPermission,
hasParent
}: MemberManagerInputPropsType & {
children: (props: ChildrenProps) => ReactNode;
refetchResource?: () => void;
isInheritPermission?: boolean;
hasParent?: boolean;
}) => {
const onUpdateCollaboratorsThen = async (props: UpdateClbPermissionProps) => {
await onUpdateCollaborators(props);
refetchCollaboratorList();
};
const onDelOneCollaboratorThen = async (tmbId: string) => {
await onDelOneCollaborator(tmbId);
refetchCollaboratorList();
};
const { feConfigs } = useSystemStore();
const { commonT } = useI18n();
const {
data: collaboratorList = [],
@@ -86,19 +104,10 @@ const CollaboratorContextProvider = ({
},
{
manual: false,
refreshDeps
refreshDeps: refreshDeps
}
);
const onUpdateCollaboratorsThen = async (props: UpdateClbPermissionProps) => {
await onUpdateCollaborators(props);
refetchCollaboratorList();
};
const onDelOneCollaboratorThen = async (tmbId: string) => {
await onDelOneCollaborator(tmbId);
refetchCollaboratorList();
};
const getPerLabelList = useCallback(
(per: PermissionValueType) => {
const Per = new Permission({ per });
@@ -125,6 +134,7 @@ const CollaboratorContextProvider = ({
[permissionList]
);
const { ConfirmModal, openConfirm } = useConfirm({});
const {
isOpen: isOpenAddMember,
onOpen: onOpenAddMember,
@@ -147,11 +157,57 @@ const CollaboratorContextProvider = ({
onDelOneCollaborator: onDelOneCollaboratorThen,
getPerLabelList
};
const onOpenAddMemberModal = () => {
if (isInheritPermission && hasParent) {
openConfirm(
() => {
onOpenAddMember();
},
undefined,
commonT('permission.Remove InheritPermission Confirm')
)();
} else {
onOpenAddMember();
}
};
const onOpenManageModalModal = () => {
if (isInheritPermission && hasParent) {
openConfirm(
() => {
onOpenManageModal();
},
undefined,
commonT('permission.Remove InheritPermission Confirm')
)();
} else {
onOpenManageModal();
}
};
return (
<CollaboratorContext.Provider value={contextValue}>
{children({ onOpenAddMember, onOpenManageModal, MemberListCard })}
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
{children({
onOpenAddMember: onOpenAddMemberModal,
onOpenManageModal: onOpenManageModalModal,
MemberListCard
})}
{isOpenAddMember && (
<AddMemberModal
onClose={() => {
onCloseAddMember();
refetchResource?.();
}}
/>
)}
{isOpenManageModal && (
<ManageModal
onClose={() => {
onCloseManageModal();
refetchResource?.();
}}
/>
)}
<ConfirmModal />
</CollaboratorContext.Provider>
);
};

View File

@@ -0,0 +1,47 @@
import { useI18n } from '@/web/context/I18n';
import { Box, BoxProps } from '@chakra-ui/react';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useToast } from '@fastgpt/web/hooks/useToast';
import React from 'react';
const ResumeInherit = ({
onResume,
...props
}: BoxProps & {
onResume?: () => Promise<any> | any;
}) => {
const { commonT } = useI18n();
const { toast } = useToast();
const { ConfirmModal: CommonConfirmModal, openConfirm: openCommonConfirm } = useConfirm({});
return onResume ? (
<Box display={'inline'} fontSize={'sm'} {...props}>
{commonT('permission.No InheritPermission')}
<Box
display={'inline'}
textDecoration={'underline'}
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={() => {
openCommonConfirm(
() =>
onResume()?.then(() => {
toast({
title: commonT('permission.Resume InheritPermission Success'),
status: 'success'
});
}),
undefined,
commonT('permission.Resume InheritPermission Confirm')
)();
}}
>
{commonT('Click to resume')}
</Box>
<CommonConfirmModal />
</Box>
) : null;
};
export default ResumeInherit;

View File

@@ -22,14 +22,4 @@ export type GetDatasetCollectionsProps = RequestPaging & {
selectFolder?: boolean;
};
export type UpdateDatasetCollectionParams = {
id: string;
parentId?: string;
name?: string;
};
/* ==== data ===== */
export type GetDatasetDataListProps = RequestPaging & {
searchText?: string;
collectionId: string;
};

View File

@@ -1,5 +1,4 @@
import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type.d';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AdminFbkType, ChatItemType } from '@fastgpt/global/core/chat/type';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';

View File

@@ -37,15 +37,6 @@ export type InsertOneDatasetDataProps = PushDatasetDataChunkProps & {
collectionId: string;
};
export type UpdateDatasetDataProps = {
id: string;
q?: string; // embedding content
a?: string; // bonus content
indexes: (Omit<DatasetDataIndexItemType, 'dataId'> & {
dataId?: string; // pg data id
})[];
};
export type GetTrainingQueueProps = {
vectorModel: string;
agentModel: string;

View File

@@ -10,16 +10,21 @@ import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/co
/* ================= collection ===================== */
export type DatasetCollectionsListItemType = {
_id: string;
parentId?: string;
tmbId: string;
name: string;
parentId?: DatasetCollectionSchemaType['parentId'];
tmbId: DatasetCollectionSchemaType['tmbId'];
name: DatasetCollectionSchemaType['name'];
type: DatasetCollectionSchemaType['type'];
updateTime: Date;
dataAmount: number;
trainingAmount: number;
createTime: DatasetCollectionSchemaType['createTime'];
updateTime: DatasetCollectionSchemaType['updateTime'];
forbid?: DatasetCollectionSchemaType['forbid'];
trainingType?: DatasetCollectionSchemaType['trainingType'];
fileId?: string;
rawLink?: string;
permission: DatasetPermission;
dataAmount: number;
trainingAmount: number;
};
/* ================= data ===================== */
@@ -30,5 +35,5 @@ export type DatasetDataListItemType = {
q: string; // embedding content
a: string; // bonus content
chunkIndex?: number;
indexes: DatasetDataSchemaType['indexes'];
// indexes: DatasetDataSchemaType['indexes'];
};

View File

@@ -37,17 +37,21 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const BillTable = () => {
const { t } = useTranslation();
const { toast } = useToast();
const [billType, setBillType] = useState<`${BillTypeEnum}` | ''>('');
const [billType, setBillType] = useState<BillTypeEnum | ''>('');
const [billDetail, setBillDetail] = useState<BillSchemaType>();
const billTypeList = useMemo(
() => [
{ label: t('common.All'), value: '' },
...Object.entries(billTypeMap).map(([key, value]) => ({
label: t(value.label),
value: key
}))
],
() =>
[
{ label: t('common.All'), value: '' },
...Object.entries(billTypeMap).map(([key, value]) => ({
label: t(value.label),
value: key
}))
] as {
label: string;
value: BillTypeEnum | '';
}[],
[t]
);
@@ -106,7 +110,7 @@ const BillTable = () => {
<Tr>
<Th>#</Th>
<Th>
<MySelect
<MySelect<BillTypeEnum | ''>
list={billTypeList}
value={billType}
size={'sm'}

View File

@@ -661,7 +661,7 @@ const Other = () => {
h={'48px'}
fontSize={'sm'}
>
{t('system.Concat us')}
</Button>
)}
</Grid>

View File

@@ -40,19 +40,23 @@ const UsageTable = () => {
from: addDays(new Date(), -7),
to: new Date()
});
const [usageSource, setUsageSource] = useState<`${UsageSourceEnum}` | ''>('');
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
const { isPc } = useSystemStore();
const { userInfo } = useUserStore();
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
const sourceList = useMemo(
() => [
{ label: t('common.All'), value: '' },
...Object.entries(UsageSourceMap).map(([key, value]) => ({
label: t(value.label),
value: key
}))
],
() =>
[
{ label: t('common.All'), value: '' },
...Object.entries(UsageSourceMap).map(([key, value]) => ({
label: t(value.label),
value: key
}))
] as {
label: never;
value: UsageSourceEnum | '';
}[],
[t]
);
@@ -144,7 +148,7 @@ const UsageTable = () => {
{/* <Th>{t('user.team.Member Name')}</Th> */}
<Th>{t('user.Time')}</Th>
<Th>
<MySelect
<MySelect<UsageSourceEnum | ''>
list={sourceList}
value={usageSource}
size={'sm'}

View File

@@ -0,0 +1,34 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { PgClient } from '@fastgpt/service/common/vectorStore/pg';
import { MongoApp } from '@fastgpt/service/core/app/schema';
/* pg 中的数据搬到 mongo dataset.datas 中,并做映射 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
await authCert({ req, authRoot: true });
await MongoApp.updateMany(
{},
{
$set: {
inheritPermission: true
}
}
);
jsonRes(res, {
message: 'success'
});
} catch (error) {
console.log(error);
jsonRes(res, {
code: 500,
error
});
}
}

View File

@@ -13,6 +13,7 @@ import { exit } from 'process';
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
import { initFastGPTConfig } from '@fastgpt/service/common/system/tools';
import json5 from 'json5';
import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await getInitConfig();
@@ -69,10 +70,6 @@ export async function getInitConfig() {
// abandon
getSystemPluginV1()
]);
console.log({
communityPlugins: global.communityPlugins
});
} catch (error) {
console.error('Load init config error', error);
global.systemInitd = false;
@@ -155,16 +152,17 @@ function getSystemPlugin() {
const filterFiles = files.filter((item) => item.endsWith('.json'));
// read json file
const fileTemplates: (PluginTemplateType & { weight: number })[] = filterFiles.map((filename) => {
const fileTemplates = filterFiles.map<SystemPluginTemplateItemType>((filename) => {
const content = readFileSync(`${basePath}/${filename}`, 'utf-8');
return {
...json5.parse(content),
id: `${PluginSourceEnum.community}-${filename.replace('.json', '')}`,
source: PluginSourceEnum.community
originCost: 0,
currentCost: 0,
id: `${PluginSourceEnum.community}-${filename.replace('.json', '')}`
};
});
fileTemplates.sort((a, b) => b.weight - a.weight);
fileTemplates.sort((a, b) => (b.weight || 0) - (a.weight || 0));
global.communityPlugins = fileTemplates;
}

View File

@@ -1,6 +1,4 @@
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { checkTeamAppLimit } from '@fastgpt/service/support/permission/teamLimit';
@@ -14,6 +12,8 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
import { ClientSession } from '@fastgpt/service/common/mongo';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
export type CreateAppBody = {
parentId?: ParentIdType;
@@ -24,15 +24,20 @@ export type CreateAppBody = {
edges?: AppSchema['edges'];
};
async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse<any>) {
async function handler(req: ApiRequestProps<CreateAppBody>) {
const { parentId, name, avatar, type, modules, edges } = req.body;
if (!name || !type || !Array.isArray(modules)) {
throw new Error('缺少参数');
return Promise.reject(CommonErrEnum.inheritPermissionError);
}
// 凭证校验
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
if (parentId) {
// if it is not a root app
// check the parent folder permission
await authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true });
}
// 上限校验
await checkTeamAppLimit(teamId);
@@ -49,9 +54,7 @@ async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse
tmbId
});
jsonRes(res, {
data: appId
});
return appId;
}
export default NextAPI(handler);
@@ -102,7 +105,7 @@ export const onCreateApp = async ({
{ session }
);
if (type !== AppTypeEnum.folder && type !== AppTypeEnum.httpPlugin) {
if (!AppFolderTypeList.includes(type!)) {
await MongoAppVersion.create(
[
{

View File

@@ -1,14 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
/* 获取我的模型 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('参数错误');
Promise.reject(CommonErrEnum.missingParams);
}
// 凭证校验
const { app } = await authApp({ req, authToken: true, appId, per: ReadPermissionVal });

View File

@@ -1,13 +1,21 @@
import type { NextApiResponse } from 'next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import {
PerResourceTypeEnum,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
import { NextAPI } from '@/service/middleware/entry';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
export type CreateAppFolderBody = {
parentId?: ParentIdType;
@@ -15,25 +23,59 @@ export type CreateAppFolderBody = {
intro?: string;
};
async function handler(req: ApiRequestProps<CreateAppFolderBody>, res: NextApiResponse<any>) {
async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
const { name, intro, parentId } = req.body;
if (!name) {
throw new Error('缺少参数');
Promise.reject(CommonErrEnum.missingParams);
}
// 凭证校验
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const parentApp = await (async () => {
if (parentId) {
// if it is not a root folder
return (
await authApp({
req,
appId: parentId,
per: WritePermissionVal,
authToken: true
})
).app; // check the parent folder permission
}
})();
// Create app
await MongoApp.create({
...parseParentIdInMongo(parentId),
avatar: FolderImgUrl,
name,
intro,
teamId,
tmbId,
type: AppTypeEnum.folder
await mongoSessionRun(async (session) => {
const app = await MongoApp.create({
...parseParentIdInMongo(parentId),
avatar: FolderImgUrl,
name,
intro,
teamId,
tmbId,
type: AppTypeEnum.folder,
// inheritPermission: !!parentApp ? true : false,
defaultPermission: !!parentApp ? parentApp.defaultPermission : AppDefaultPermissionVal
});
if (parentId) {
const parentClbs = await getResourceAllClbs({
teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.app,
session
});
await syncCollaborators({
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: app._id,
collaborators: parentClbs,
session
});
}
});
}

View File

@@ -1,7 +1,5 @@
import type { NextApiResponse } from 'next';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppListItemType } from '@fastgpt/global/core/app/type';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { NextAPI } from '@/service/middleware/entry';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import {
@@ -12,8 +10,10 @@ import { AppPermission } from '@fastgpt/global/support/permission/app/controller
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
export type ListAppBody = {
parentId?: ParentIdType;
@@ -22,22 +22,34 @@ export type ListAppBody = {
searchKey?: string;
};
async function handler(
req: ApiRequestProps<ListAppBody>,
res: NextApiResponse<any>
): Promise<AppListItemType[]> {
async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemType[]> {
const { parentId, type, getRecentlyChat, searchKey } = req.body;
// 凭证校验
const {
teamId,
app: ParentApp,
tmbId,
teamId,
permission: tmbPer
} = await authUserPer({
req,
authToken: true,
per: ReadPermissionVal
});
const { parentId, type, getRecentlyChat, searchKey } = req.body;
} = await (async () => {
if (parentId) {
return await authApp({
req,
authToken: true,
appId: parentId,
per: ReadPermissionVal
});
} else {
return {
...(await authUserPer({
req,
authToken: true,
per: ReadPermissionVal
})),
app: undefined
};
}
})();
const findAppsQuery = (() => {
const searchMatch = searchKey
@@ -71,7 +83,7 @@ async function handler(
const [myApps, rpList] = await Promise.all([
MongoApp.find(
findAppsQuery,
'_id avatar type name intro tmbId updateTime pluginData defaultPermission'
'_id parentId avatar type name intro tmbId updateTime pluginData defaultPermission inheritPermission'
)
.sort({
updateTime: -1
@@ -87,11 +99,29 @@ async function handler(
const filterApps = myApps
.map((app) => {
const perVal = rpList.find((item) => String(item.resourceId) === String(app._id))?.permission;
const Per = new AppPermission({
per: perVal ?? app.defaultPermission,
isOwner: String(app.tmbId) === tmbId || tmbPer.isOwner
});
const Per = (() => {
// Inherit app
if (app.inheritPermission && ParentApp && !AppFolderTypeList.includes(app.type)) {
// get its parent's permission as its permission
app.defaultPermission = ParentApp.defaultPermission;
const perVal = rpList.find(
(item) => String(item.resourceId) === String(ParentApp._id)
)?.permission;
return new AppPermission({
per: perVal ?? app.defaultPermission,
isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner
});
} else {
const perVal = rpList.find(
(item) => String(item.resourceId) === String(app._id)
)?.permission;
return new AppPermission({
per: perVal ?? app.defaultPermission,
isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner
});
}
})();
return {
...app,
@@ -112,7 +142,8 @@ async function handler(
updateTime: app.updateTime,
permission: app.permission,
defaultPermission: app.defaultPermission || AppDefaultPermissionVal,
pluginData: app.pluginData
pluginData: app.pluginData,
inheritPermission: app.inheritPermission ?? true
}));
}

View File

@@ -6,7 +6,7 @@ import {
getPluginPreviewNode,
splitCombinePluginId
} from '@fastgpt/service/core/app/plugin/controller';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node.d';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';

View File

@@ -1,39 +1,26 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { FlowNodeTypeEnum, defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { NextAPI } from '@/service/middleware/entry';
import { getCommunityPluginsTemplateList } from '@fastgpt/plugins/register';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
await authCert({ req, authToken: true });
async function handler(
req: NextApiRequest,
res: NextApiResponse<any>
): Promise<NodeTemplateListItemType[]> {
await authCert({ req, authToken: true });
const data: FlowNodeTemplateType[] =
global.communityPlugins?.map((plugin) => ({
id: plugin.id,
pluginId: plugin.id,
templateType: plugin.templateType ?? FlowNodeTemplateTypeEnum.other,
flowNodeType: FlowNodeTypeEnum.pluginModule,
avatar: plugin.avatar,
name: plugin.name,
intro: plugin.intro,
showStatus: true,
isTool: plugin.isTool,
version: defaultNodeVersion,
inputs: [],
outputs: []
})) || [];
// const data: NodeTemplateListItemType[] =
// global.communityPlugins?.map((plugin) => ({
// id: plugin.id,
// templateType: plugin.templateType ?? FlowNodeTemplateTypeEnum.other,
// flowNodeType: FlowNodeTypeEnum.pluginModule,
// avatar: plugin.avatar,
// name: plugin.name,
// intro: plugin.intro
// })) || [];
jsonRes<FlowNodeTemplateType[]>(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
return getCommunityPluginsTemplateList();
}
export default NextAPI(handler);

View File

@@ -0,0 +1,45 @@
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import {
ManagePermissionVal,
PerResourceTypeEnum
} from '@fastgpt/global/support/permission/constant';
import { resumeInheritPermission } from '@fastgpt/service/support/permission/inheritPermission';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
export type ResumeInheritPermissionQuery = {
appId: string;
};
export type ResumeInheritPermissionBody = {};
// resume the app's inherit permission.
async function handler(
req: ApiRequestProps<ResumeInheritPermissionBody, ResumeInheritPermissionQuery>
) {
const { appId } = req.query;
const { app } = await authApp({
appId,
req,
authToken: true,
per: ManagePermissionVal
});
if (app.parentId) {
await resumeInheritPermission({
resource: app,
folderTypeList: AppFolderTypeList,
resourceType: PerResourceTypeEnum.app,
resourceModel: MongoApp
});
} else {
await MongoApp.updateOne(
{
_id: appId
},
{
inheritPermission: true
}
);
}
}
export default NextAPI(handler);

View File

@@ -1,4 +1,3 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { AppUpdateParams } from '@/global/core/app/api';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
@@ -6,12 +5,37 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
PerResourceTypeEnum,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import {
syncChildrenPermission,
syncCollaborators
} from '@fastgpt/service/support/permission/inheritPermission';
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
import { ClientSession } from 'mongoose';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
/* 获取我的模型 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
/*
修改默认权限
1. 继承态目录:关闭继承态,修改权限,同步子目录默认权限
2. 继承态资源:关闭继承态,修改权限, 复制父级协作者。
3. 非继承目录:修改权限,同步子目录默认权限
4. 非继承资源:修改权限
移动
1. 继承态目录:改 parentId, 修改成父的默认权限,同步子目录默认权限和协作者
2. 继承态资源:改 parentId
3. 非继承:改 parentId
*/
async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>) {
const {
parentId,
name,
@@ -24,40 +48,152 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
teamTags,
defaultPermission
} = req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('appId is empty');
Promise.reject(CommonErrEnum.missingParams);
}
// 凭证校验
if (defaultPermission) {
await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
} else {
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
}
const { app } = await (async () => {
if (defaultPermission !== undefined) {
// if defaultPermission or inheritPermission is set, then need manage permission
return authApp({ req, authToken: true, appId, per: ManagePermissionVal });
} else {
return authApp({ req, authToken: true, appId, per: WritePermissionVal });
}
})();
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
const isDefaultPermissionChanged =
defaultPermission !== undefined && defaultPermission !== app.defaultPermission;
const isFolder = AppFolderTypeList.includes(app.type);
// 更新模型
await MongoApp.findByIdAndUpdate(appId, {
...parseParentIdInMongo(parentId),
...(name && { name }),
...(type && { type }),
...(avatar && { avatar }),
...(intro !== undefined && { intro }),
...(defaultPermission !== undefined && { defaultPermission }),
...(teamTags && { teamTags }),
...(formatNodes && {
modules: formatNodes
}),
...(edges && {
edges
}),
...(chatConfig && { chatConfig })
});
const onUpdate = async (
session?: ClientSession,
updatedDefaultPermission?: PermissionValueType
) => {
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
return MongoApp.findByIdAndUpdate(
appId,
{
...parseParentIdInMongo(parentId),
...(name && { name }),
...(type && { type }),
...(avatar && { avatar }),
...(intro !== undefined && { intro }),
// update default permission(Maybe move update)
...(updatedDefaultPermission !== undefined && {
defaultPermission: updatedDefaultPermission
}),
// Not root, update default permission
...(isDefaultPermissionChanged && { inheritPermission: false }),
...(teamTags && { teamTags }),
...(formatNodes && {
modules: formatNodes
}),
...(edges && {
edges
}),
...(chatConfig && { chatConfig })
},
{ session }
);
};
// Move
if (parentId !== undefined) {
await mongoSessionRun(async (session) => {
// Auth
const parentDefaultPermission = await (async () => {
if (parentId) {
const { app: parentApp } = await authApp({
req,
authToken: true,
appId: parentId,
per: WritePermissionVal
});
return parentApp.defaultPermission;
}
return AppDefaultPermissionVal;
})();
// Inherit folder: Sync children permission and it's clbs
if (isFolder && app.inheritPermission) {
const parentClbs = await getResourceAllClbs({
teamId: app.teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.app,
session
});
// sync self
await syncCollaborators({
resourceId: app._id,
resourceType: PerResourceTypeEnum.app,
collaborators: parentClbs,
session,
teamId: app.teamId
});
// sync the children
await syncChildrenPermission({
resource: app,
resourceType: PerResourceTypeEnum.app,
resourceModel: MongoApp,
folderTypeList: AppFolderTypeList,
defaultPermission: parentDefaultPermission,
collaborators: parentClbs,
session
});
return onUpdate(session, parentDefaultPermission);
}
return onUpdate(session);
});
}
// Update default permission
if (isDefaultPermissionChanged) {
await mongoSessionRun(async (session) => {
if (isFolder) {
// Sync children default permission
await syncChildrenPermission({
resource: {
_id: app._id,
type: app.type,
teamId: app.teamId,
parentId: app.parentId
},
folderTypeList: AppFolderTypeList,
resourceModel: MongoApp,
resourceType: PerResourceTypeEnum.app,
session,
defaultPermission
});
} else if (app.inheritPermission && app.parentId) {
// Inherit app
const parentClbs = await getResourceAllClbs({
teamId: app.teamId,
resourceId: app.parentId,
resourceType: PerResourceTypeEnum.app,
session
});
await syncCollaborators({
resourceId: app._id,
resourceType: PerResourceTypeEnum.app,
collaborators: parentClbs,
session,
teamId: app.teamId
});
}
return onUpdate(session, defaultPermission);
});
}
}
export default NextAPI(handler);

View File

@@ -5,7 +5,7 @@ import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
export type getLatestVersionQuery = {
appId: string;

View File

@@ -10,8 +10,9 @@ import { DatasetDataCollectionName } from '@fastgpt/service/core/dataset/data/sc
import { startTrainingQueue } from '@/service/core/dataset/training/utils';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { PagingData } from '@/types';
async function handler(req: NextApiRequest) {
async function handler(req: NextApiRequest): Promise<PagingData<DatasetCollectionsListItemType>> {
let {
pageNum = 1,
pageSize = 10,
@@ -45,9 +46,24 @@ async function handler(req: NextApiRequest) {
: {})
};
const selectField = {
_id: 1,
parentId: 1,
tmbId: 1,
name: 1,
type: 1,
forbid: 1,
createTime: 1,
updateTime: 1,
trainingType: 1,
fileId: 1,
rawLink: 1
};
// not count data amount
if (simple) {
const collections = await MongoDatasetCollection.find(match, '_id parentId type name')
const collections = await MongoDatasetCollection.find(match)
.select(selectField)
.sort({
updateTime: -1
})
@@ -123,15 +139,7 @@ async function handler(req: NextApiRequest) {
},
{
$project: {
_id: 1,
parentId: 1,
tmbId: 1,
name: 1,
type: 1,
status: 1,
updateTime: 1,
fileId: 1,
rawLink: 1,
...selectField,
dataAmount: {
$ifNull: [{ $arrayElemAt: ['$dataCount.count', 0] }, 0]
},

View File

@@ -1,9 +1,11 @@
import type { NextApiRequest } from 'next';
import { getDatasetCollectionPaths } from '@fastgpt/service/core/dataset/collection/utils';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { NextAPI } from '@/service/middleware/entry';
export default async function handler(req: NextApiRequest) {
async function handler(req: NextApiRequest) {
const { parentId } = req.query as { parentId: string };
if (!parentId) {
@@ -16,9 +18,34 @@ export default async function handler(req: NextApiRequest) {
collectionId: parentId,
per: ReadPermissionVal
});
const paths = await getDatasetCollectionPaths({
parentId
});
return paths;
}
export default NextAPI(handler);
export async function getDatasetCollectionPaths({
parentId = ''
}: {
parentId?: string;
}): Promise<ParentTreePathItemType[]> {
async function find(parentId?: string): Promise<ParentTreePathItemType[]> {
if (!parentId) {
return [];
}
const parent = await MongoDatasetCollection.findOne({ _id: parentId }, 'name parentId');
if (!parent) return [];
const paths = await find(parent.parentId);
paths.push({ parentId, parentName: parent.name });
return paths;
}
return find(parentId);
}

View File

@@ -1,14 +1,20 @@
import type { NextApiRequest } from 'next';
import type { UpdateDatasetCollectionParams } from '@/global/core/api/datasetReq.d';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { getCollectionUpdateTime } from '@fastgpt/service/core/dataset/collection/utils';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { ApiRequestProps } from '@fastgpt/service/type/next';
async function handler(req: NextApiRequest) {
const { id, parentId, name } = req.body as UpdateDatasetCollectionParams;
export type UpdateDatasetCollectionParams = {
id: string;
parentId?: string;
name?: string;
forbid?: boolean;
};
async function handler(req: ApiRequestProps<UpdateDatasetCollectionParams>) {
const { id, parentId, name, forbid } = req.body;
if (!id) {
return Promise.reject(CommonErrEnum.missingParams);
@@ -25,7 +31,8 @@ async function handler(req: NextApiRequest) {
const updateFields: Record<string, any> = {
...(parentId !== undefined && { parentId: parentId || null }),
...(name && { name, updateTime: getCollectionUpdateTime({ name }) })
...(name && { name, updateTime: getCollectionUpdateTime({ name }) }),
...(forbid !== undefined && { forbid })
};
await MongoDatasetCollection.findByIdAndUpdate(id, {

View File

@@ -1,18 +1,21 @@
import type { NextApiRequest } from 'next';
import type { GetDatasetDataListProps } from '@/global/core/api/datasetReq';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { PagingData, RequestPaging } from '@/types';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { DatasetDataListItemType } from '@/global/core/dataset/type';
async function handler(req: NextApiRequest) {
let {
pageNum = 1,
pageSize = 10,
searchText = '',
collectionId
} = req.body as GetDatasetDataListProps;
export type GetDatasetDataListProps = RequestPaging & {
searchText?: string;
collectionId: string;
};
async function handler(
req: ApiRequestProps<GetDatasetDataListProps>
): Promise<PagingData<DatasetDataListItemType>> {
let { pageNum = 1, pageSize = 10, searchText = '', collectionId } = req.body;
pageSize = Math.min(pageSize, 30);
@@ -25,15 +28,14 @@ async function handler(req: NextApiRequest) {
per: ReadPermissionVal
});
searchText = replaceRegChars(searchText).replace(/'/g, '');
const queryReg = new RegExp(`${replaceRegChars(searchText)}`, 'i');
const match = {
teamId,
datasetId: collection.datasetId._id,
collectionId,
...(searchText
...(searchText.trim()
? {
$or: [{ q: new RegExp(searchText, 'i') }, { a: new RegExp(searchText, 'i') }]
$or: [{ q: queryReg }, { a: queryReg }]
}
: {})
};

View File

@@ -1,14 +1,14 @@
import type { NextApiRequest } from 'next';
import { updateData2Dataset } from '@/service/core/dataset/data/controller';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { UpdateDatasetDataProps } from '@/global/core/dataset/api';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { UpdateDatasetDataProps } from '@fastgpt/global/core/dataset/controller';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { authDatasetData } from '@fastgpt/service/support/permission/dataset/auth';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
async function handler(req: NextApiRequest) {
const { id, q = '', a, indexes = [] } = req.body as UpdateDatasetDataProps;
async function handler(req: ApiRequestProps<UpdateDatasetDataProps>) {
const { dataId, q, a, indexes = [] } = req.body;
// auth data permission
const {
@@ -21,30 +21,30 @@ async function handler(req: NextApiRequest) {
req,
authToken: true,
authApiKey: true,
dataId: id,
dataId,
per: WritePermissionVal
});
// auth team balance
await checkDatasetLimit({
teamId,
insertLen: 1
});
if (q || a || indexes.length > 0) {
const { tokens } = await updateData2Dataset({
dataId,
q,
a,
indexes,
model: vectorModel
});
const { tokens } = await updateData2Dataset({
dataId: id,
q,
a,
indexes,
model: vectorModel
});
pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: vectorModel
});
pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: vectorModel
});
} else {
// await MongoDatasetData.findByIdAndUpdate(dataId, {
// ...(forbid !== undefined && { forbid })
// });
}
}
export default NextAPI(handler);

View File

@@ -27,7 +27,7 @@ async function handler(
}
/* user auth */
const [{ teamId, tmbId }] = await Promise.all([
const [{ teamId, tmbId }, { app }] = await Promise.all([
authCert({
req,
authToken: true
@@ -38,12 +38,6 @@ async function handler(
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
const app = {
...defaultApp,
teamId,
tmbId
};
/* start process */
const { flowUsages, flowResponses, debugResponse } = await dispatchWorkFlow({
res,

View File

@@ -3,17 +3,25 @@ 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';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { SystemVariablesType } from '@fastgpt/global/core/workflow/runtime/type';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
type Props = HttpBodyType<{
customFeedback: string;
}>;
type Props = HttpBodyType<
SystemVariablesType & {
customFeedback: string;
customInputs: Record<string, any>;
}
>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
customFeedback,
[NodeInputKeyEnum.addInputParam]: { appId, chatId, responseChatItemId: chatItemId }
appId,
chatId,
responseChatItemId: chatItemId,
customInputs
} = req.body as Props;
await authRequestFromLocal({ req });
@@ -22,25 +30,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
return res.json({});
}
const feedbackText = replaceVariable(customFeedback, customInputs);
// wait the chat finish
setTimeout(() => {
addCustomFeedbacks({
appId,
chatId,
chatItemId,
feedbacks: [customFeedback]
feedbacks: [feedbackText]
});
}, 60000);
if (!chatId || !chatItemId) {
return res.json({
[NodeOutputKeyEnum.answerText]: `\\n\\n**自动反馈调试**: "${customFeedback}"\\n\\n`,
text: customFeedback
[NodeOutputKeyEnum.answerText]: `\\n\\n**自动反馈调试**: "${feedbackText}"\\n\\n`,
text: feedbackText
});
}
res.json({
text: customFeedback
text: feedbackText
});
} catch (err) {
console.log(err);

View File

@@ -1,18 +1,18 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { 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';
import { ApiRequestProps } from '@fastgpt/service/type/next';
type Props = HttpBodyType<{
text: string;
[key: string]: any;
customInputs: Record<string, any>;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
export default async function handler(req: ApiRequestProps<Props>, res: NextApiResponse<any>) {
try {
const { text, [NodeInputKeyEnum.addInputParam]: obj } = req.body as Props;
const { text, customInputs: obj = {} } = req.body;
await authRequestFromLocal({ req });

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useCallback } from 'react';
import {
Box,
Flex,
@@ -15,7 +15,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
@@ -32,15 +32,19 @@ import {
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { resumeInheritPer } from '@/web/core/app/api';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { commonT } = useI18n();
const { toast } = useToast();
const { updateAppDetail, appDetail } = useContextSelector(AppContext, (v) => v);
const { updateAppDetail, appDetail, reloadApp } = useContextSelector(AppContext, (v) => v);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
@@ -52,12 +56,10 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
setValue,
getValues,
formState: { errors },
handleSubmit,
watch
handleSubmit
} = useForm({
defaultValues: appDetail
});
const defaultPermission = watch('defaultPermission');
const avatar = getValues('avatar');
// submit config
@@ -76,6 +78,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
title: t('common.Update Success'),
status: 'success'
});
reloadApp();
},
errorToast: t('common.Update Failed')
});
@@ -138,6 +141,17 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
});
};
const { runAsync: resumeInheritPermission } = useRequest2(
() => resumeInheritPer(appDetail._id),
// () => putAppById(appDetail._id, { inheritPermission: true }),
{
errorToast: '恢复失败',
onSuccess: () => {
reloadApp();
}
}
);
return (
<MyModal
isOpen={true}
@@ -182,13 +196,23 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
{/* role */}
{appDetail.permission.hasManagePer && (
<>
{!appDetail.inheritPermission && appDetail.parentId && (
<Box mt={3}>
<ResumeInherit onResume={resumeInheritPermission} />
</Box>
)}
<Box mt="4">
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
<DefaultPermissionList
mt="2"
per={defaultPermission}
per={appDetail.defaultPermission}
defaultPer={AppDefaultPermissionVal}
onChange={(v) => setValue('defaultPermission', v)}
isInheritPermission={appDetail.inheritPermission}
onChange={(v) => {
setValue('defaultPermission', v);
handleSubmit((data) => saveSubmitSuccess(data), saveSubmitError)();
}}
hasParent={!!appDetail.parentId}
/>
</Box>
<Box mt={6}>
@@ -198,6 +222,9 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
permissionList={AppPermissionList}
onUpdateCollaborators={onUpdateCollaborators}
onDelOneCollaborator={onDelCollaborator}
refreshDeps={[appDetail.inheritPermission]}
isInheritPermission={appDetail.inheritPermission}
hasParent={!!appDetail.parentId}
>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
@@ -208,7 +235,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
justifyContent="space-between"
w="full"
>
<Box fontSize={'sm'}></Box>
<Box fontSize={'sm'}>{commonT('permission.Collaborator')}</Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"

View File

@@ -54,7 +54,6 @@ const Edit = ({
flex={'1 0 0'}
h={0}
pt={[2, 1.5]}
pl={[2, 1]}
gap={1}
borderRadius={'lg'}
overflowY={['auto', 'unset']}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useTransition } from 'react';
import React, { useMemo, useTransition } from 'react';
import {
Box,
Flex,
@@ -9,8 +9,7 @@ import {
Button,
HStack
} from '@chakra-ui/react';
import { AddIcon, SmallAddIcon } from '@chakra-ui/icons';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
@@ -468,7 +467,7 @@ const EditForm = ({
onRemoveTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((item) => item.id !== e.id)
selectedTools: state.selectedTools.filter((item) => item.pluginId !== e.id)
}));
}}
onClose={onCloseToolsSelect}

View File

@@ -89,7 +89,7 @@ const Header = ({
<RouteTab />
</Flex>
)}
<Flex pl={2} pt={[2, 3]} alignItems={'flex-start'} position={'relative'}>
<Flex pt={[2, 3]} alignItems={'flex-start'} position={'relative'}>
<Box flex={'1'}>
<FolderPath paths={paths} hoverStyle={{ color: 'primary.600' }} onClick={onclickRoute} />
</Box>
@@ -102,20 +102,23 @@ const Header = ({
<Flex alignItems={'center'}>
{!historiesDefaultData && (
<>
<MyTag
mr={3}
type={'borderFill'}
showDot
colorSchema={
isPublished
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{isPublished
? publishStatusStyle.published.text
: publishStatusStyle.unPublish.text}
</MyTag>
{isPc && (
<MyTag
mr={3}
type={'borderFill'}
showDot
colorSchema={
isPublished
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{isPublished
? publishStatusStyle.published.text
: publishStatusStyle.unPublish.text}
</MyTag>
)}
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}

View File

@@ -22,14 +22,16 @@ import {
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import {
FlowNodeTemplateType,
NodeTemplateListItemType
} from '@fastgpt/global/core/workflow/type/node.d';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { AddIcon } from '@chakra-ui/icons';
import { getPreviewPluginNode, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { useForm } from 'react-hook-form';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
@@ -43,7 +45,7 @@ import FolderPath from '@/components/common/folder/Path';
type Props = {
selectedTools: FlowNodeTemplateType[];
onAddTool: (tool: FlowNodeTemplateType) => void;
onRemoveTool: (tool: FlowNodeTemplateType) => void;
onRemoveTool: (tool: NodeTemplateListItemType) => void;
};
enum TemplateTypeEnum {
@@ -160,7 +162,7 @@ const RenderList = React.memo(function RenderList({
onRemoveTool,
setParentId
}: Props & {
templates: FlowNodeTemplateType[];
templates: NodeTemplateListItemType[];
isLoadingData: boolean;
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
}) {
@@ -170,29 +172,10 @@ const RenderList = React.memo(function RenderList({
const { register, getValues, setValue, handleSubmit, reset } = useForm<Record<string, any>>({});
const checkToolInputValid = useCallback((tool: FlowNodeTemplateType) => {
for (const input of tool.inputs) {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
if (renderType === FlowNodeInputTypeEnum.addInputParam) {
return false;
}
}
return true;
}, []);
const filterValidTools = useMemo(
() => templates.filter(checkToolInputValid),
[checkToolInputValid, templates]
);
const { mutate: onClickAdd, isLoading } = useRequest({
mutationFn: async (template: FlowNodeTemplateType) => {
const res = await getPreviewPluginNode({ appId: template.id });
if (!checkToolInputValid(res)) {
return Promise.reject(t('core.app.ToolCall.This plugin cannot be called as a tool'));
}
// All input is tool params
if (res.inputs.every((input) => input.toolDescription)) {
onAddTool(res);
@@ -204,12 +187,12 @@ const RenderList = React.memo(function RenderList({
errorToast: t('core.module.templates.Load plugin error')
});
return filterValidTools.length === 0 && !isLoadingData ? (
return templates.length === 0 && !isLoadingData ? (
<EmptyTip text={t('core.app.ToolCall.No plugin')} />
) : (
<MyBox>
{filterValidTools.map((item, i) => {
const selected = selectedTools.some((tool) => tool.pluginId === item.pluginId);
{templates.map((item, i) => {
const selected = selectedTools.some((tool) => tool.pluginId === item.id);
return (
<Flex
@@ -247,8 +230,7 @@ const RenderList = React.memo(function RenderList({
>
{t('common.Remove')}
</Button>
) : item.pluginType === PluginTypeEnum.folder ||
item.pluginType === AppTypeEnum.httpPlugin ? (
) : item.isFolder ? (
<Button size={'sm'} variant={'whiteBase'} onClick={() => setParentId(item.id)}>
{t('common.Open')}
</Button>

View File

@@ -24,7 +24,7 @@ const SimpleEdit = () => {
});
return (
<Flex h={'100%'} flexDirection={'column'} pr={3} pb={3}>
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]} pb={3}>
<Header appForm={appForm} setAppForm={setAppForm} />
{currentTab === TabEnum.appEdit ? (
<Edit appForm={appForm} setAppForm={setAppForm} />

View File

@@ -1,4 +1,4 @@
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import React, { forwardRef, ForwardedRef } from 'react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { Box, Flex, IconButton } from '@chakra-ui/react';

View File

@@ -1,9 +1,9 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Flex, IconButton, Input, InputGroup, InputLeftElement, css } from '@chakra-ui/react';
import type {
FlowNodeTemplateType,
nodeTemplateListType
} from '@fastgpt/global/core/workflow/type/index.d';
NodeTemplateListItemType,
NodeTemplateListType
} from '@fastgpt/global/core/workflow/type/node.d';
import { useViewport, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@/components/Avatar';
@@ -30,13 +30,15 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import FolderPath from '@/components/common/folder/Path';
import { getAppFolderPath } from '@/web/core/app/api/app';
import { useWorkflowUtils } from './hooks/useUtils';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import { cloneDeep } from 'lodash';
type ModuleTemplateListProps = {
isOpen: boolean;
onClose: () => void;
};
type RenderListProps = {
templates: FlowNodeTemplateType[];
templates: NodeTemplateListItemType[];
onClose: () => void;
parentId: ParentIdType;
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
@@ -56,73 +58,44 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
const { basicNodeTemplates, hasToolNode, nodeList } = useContextSelector(
const { basicNodeTemplates, hasToolNode, nodeList, appId } = useContextSelector(
WorkflowContext,
(v) => v
);
const [pluginBuffer, setPluginBuffer] = useState<{
systemPlugin: FlowNodeTemplateType[];
teamPlugin: FlowNodeTemplateType[];
}>({
[TemplateTypeEnum.systemPlugin]: [],
[TemplateTypeEnum.teamPlugin]: []
});
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
const { data: templates = [], loading } = useRequest2(
const { data: basicNodes } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.basic) {
return basicNodeTemplates.filter((item) => {
// unique node filter
if (item.unique) {
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
if (nodeExist) {
return basicNodeTemplates
.filter((item) => {
// unique node filter
if (item.unique) {
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
if (nodeExist) {
return false;
}
}
// special node filter
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
return false;
}
}
// special node filter
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
return false;
}
// tool stop
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
return false;
}
return true;
});
// tool stop
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
return false;
}
return true;
})
.map<NodeTemplateListItemType>((item) => ({
id: item.id,
flowNodeType: item.flowNodeType,
templateType: item.templateType,
avatar: item.avatar,
name: item.name,
intro: item.intro
}));
}
if (templateType === TemplateTypeEnum.systemPlugin) {
if (pluginBuffer.systemPlugin.length === 0) {
return getSystemPlugTemplates().then((res) => {
setPluginBuffer((state) => ({
...state,
systemPlugin: res
}));
return res;
});
} else {
return pluginBuffer.systemPlugin;
}
}
if (templateType === TemplateTypeEnum.teamPlugin) {
if (pluginBuffer.teamPlugin.length === 0) {
return getTeamPlugTemplates({
parentId,
searchKey,
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
}).then((res) => {
setPluginBuffer((state) => ({
...state,
teamPlugin: res
}));
return res;
});
} else {
return pluginBuffer.teamPlugin;
}
}
return [];
},
{
manual: false,
@@ -130,6 +103,39 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
refreshDeps: [basicNodeTemplates, nodeList, hasToolNode, templateType, searchKey, parentId]
}
);
const { data: teamApps, loading: isLoadingTeamApp } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey,
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
}).then((res) => res.filter((app) => app.id !== appId));
}
},
{
manual: false,
throttleWait: 300,
refreshDeps: [templateType, searchKey, parentId]
}
);
const { data: systemPlugins, loading: isLoadingSystemPlugins } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.systemPlugin) {
return getSystemPlugTemplates();
}
},
{
manual: false,
refreshDeps: [templateType]
}
);
const isLoading = isLoadingTeamApp || isLoadingSystemPlugins;
const templates = useMemo(
() => basicNodes || teamApps || systemPlugins || [],
[basicNodes, systemPlugins, teamApps]
);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(parentId), {
manual: false,
@@ -151,7 +157,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
fontSize={'sm'}
/>
<MyBox
isLoading={loading}
isLoading={isLoading}
display={'flex'}
zIndex={3}
flexDirection={'column'}
@@ -246,7 +252,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
</MyBox>
</>
);
}, [isOpen, onClose, loading, t, templateType, searchKey, parentId, paths, templates, router]);
}, [isOpen, onClose, isLoading, t, templateType, searchKey, parentId, paths, templates, router]);
return Render;
};
@@ -270,8 +276,8 @@ const RenderList = React.memo(function RenderList({
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const { computedNewNodeName } = useWorkflowUtils();
const formatTemplates = useMemo<nodeTemplateListType>(() => {
const copy: nodeTemplateListType = JSON.parse(JSON.stringify(workflowNodeTemplateList(t)));
const formatTemplates = useMemo<NodeTemplateListType>(() => {
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList(t));
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
@@ -282,7 +288,13 @@ const RenderList = React.memo(function RenderList({
}, [templates, parentId]);
const onAddNode = useCallback(
async ({ template, position }: { template: FlowNodeTemplateType; position: XYPosition }) => {
async ({
template,
position
}: {
template: NodeTemplateListItemType;
position: XYPosition;
}) => {
if (!reactFlowWrapper?.current) return;
const templateNode = await (async () => {
@@ -297,7 +309,11 @@ const RenderList = React.memo(function RenderList({
}
// base node
return { ...template };
const baseTemplate = moduleTemplatesFlat.find((item) => item.id === template.id);
if (!baseTemplate) {
throw new Error('baseTemplate not found');
}
return { ...baseTemplate };
} catch (e) {
toast({
status: 'error',
@@ -390,7 +406,7 @@ const RenderList = React.memo(function RenderList({
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={template.pluginType !== AppTypeEnum.folder}
draggable={!template.isFolder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
@@ -399,10 +415,7 @@ const RenderList = React.memo(function RenderList({
});
}}
onClick={(e) => {
if (
template.pluginType === AppTypeEnum.folder ||
template.pluginType === AppTypeEnum.httpPlugin
) {
if (template.isFolder) {
return setParentId(template.id);
}
if (isPc) {
@@ -412,7 +425,7 @@ const RenderList = React.memo(function RenderList({
});
}
onAddNode({
template: template,
template,
position: { x: e.clientX, y: e.clientY }
});
onClose();

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/runApp/type';
import { useTranslation } from 'next-i18next';
import SelectOneResource from '@/components/common/folder/SelectOneResource';
import {

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { Box, Flex, FlexProps } from '@chakra-ui/react';
const IOTitle = ({ text }: { text?: 'Input' | 'Output' | string }) => {
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & FlexProps) => {
return (
<Flex fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3}>
<Flex fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3} {...props}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} mr={1.5} />
{text}
</Flex>

View File

@@ -1,5 +1,5 @@
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useCallback, useState } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';

View File

@@ -3,7 +3,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
import { Node } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { useWorkflowUtils } from './useUtils';

View File

@@ -18,7 +18,7 @@ import ButtonEdge from './components/ButtonEdge';
import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
@@ -30,6 +30,8 @@ const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
[FlowNodeTypeEnum.textEditor]: NodeSimple,
[FlowNodeTypeEnum.customFeedback]: NodeSimple,
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
[FlowNodeTypeEnum.chatNode]: NodeSimple,
@@ -40,8 +42,8 @@ const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginIO/PluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginIO/PluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import RenderToolInput from './render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
@@ -14,16 +13,15 @@ const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
{toolInputs.length > 0 && (
{isTool && (
<>
<IOTitle text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}

View File

@@ -2,10 +2,10 @@ import React, { useMemo } from 'react';
import { NodeProps, Position } from 'reactflow';
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/template/system/classifyQuestion/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
@@ -24,7 +24,7 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const { ConfirmModal, openConfirm } = useConfirm({
content: workflowT('code.Reset template confirm')
});
@@ -79,11 +79,10 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{toolInputs.length > 0 && (
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}

View File

@@ -1,22 +1,34 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
WorkflowIOValueTypeEnum,
NodeInputKeyEnum,
VARIABLE_NODE_ID
} from '@fastgpt/global/core/workflow/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import {
FlowNodeInputItemType,
ReferenceValueProps
} from '@fastgpt/global/core/workflow/type/io.d';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import ValueTypeLabel from './render/ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
@@ -25,10 +37,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const quotes = useMemo(
() => inputs.filter((item) => item.valueType === WorkflowIOValueTypeEnum.datasetQuote),
[inputs]
);
const quoteList = useMemo(() => inputs.filter((item) => item.canEdit), [inputs]);
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
@@ -47,14 +56,6 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return maxTokens;
}, [llmModelList, nodeList]);
const onAddField = useCallback(() => {
onChangeNode({
nodeId,
type: 'addInput',
value: getOneQuoteInputTemplate({ index: quotes.length + 1 })
});
}, [nodeId, onChangeNode, quotes.length]);
const CustomComponent = useMemo(() => {
return {
[NodeInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => (
@@ -83,26 +84,42 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
/>
</Box>
),
customComponent: (item: FlowNodeInputItemType) => (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} fontWeight={'medium'} color={'myGray.600'}>
{t('core.workflow.Dataset quote')}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onAddField}
>
{t('common.Add New')}
</Button>
</Flex>
)
[NodeInputKeyEnum.datasetQuoteList]: (item: FlowNodeInputItemType) => {
return (
<>
<HStack className="nodrag" cursor={'default'} position={'relative'}>
<HStack spacing={1} position={'relative'} fontWeight={'medium'} color={'myGray.600'}>
<Box>{t('core.workflow.Dataset quote')}</Box>
</HStack>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => {
onChangeNode({
nodeId,
type: 'addInput',
value: getOneQuoteInputTemplate({ index: quoteList.length + 1 })
});
}}
>
{t('common.Add New')}
</Button>
</HStack>
<Box mt={2}>
{quoteList.map((children) => (
<Box key={children.key} _notLast={{ mb: 3 }}>
<Reference nodeId={nodeId} inputChildren={children} />
</Box>
))}
</Box>
</>
);
}
};
}, [nodeId, onAddField, onChangeNode, t, tokenLimit]);
}, [nodeId, onChangeNode, quoteList, t, tokenLimit]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
@@ -118,3 +135,79 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
};
export default React.memo(NodeDatasetConcat);
function Reference({
nodeId,
inputChildren
}: {
nodeId: string;
inputChildren: FlowNodeInputItemType;
}) {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value:
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{inputChildren.label}</FormLabel>
{/* value */}
<ValueTypeLabel valueType={inputChildren.valueType} />
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={onDel}
/>
</Flex>
<ReferSelector
placeholder={t(inputChildren.referencePlaceholder || '选择知识库引用')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
</>
);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
const NodeEmpty = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return <NodeCard selected={selected} {...data}></NodeCard>;

View File

@@ -9,15 +9,15 @@ import {
Input,
Textarea
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { fnValueTypeSelect } from '@/web/core/workflow/constants/dataType';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
export const defaultField: ContextExtractAgentItemType = {
valueType: 'string',
@@ -74,11 +74,11 @@ const ExtractFieldModal = ({
<Flex alignItems={'center'} mt={5}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('core.module.Data Type')}</FormLabel>
<Box flex={'1 0 0'}>
<MySelect
<MySelect<string>
list={fnValueTypeSelect}
value={valueType}
onchange={(e: any) => {
setValue('valueType', e);
onchange={(e) => {
setValue('valueType', e as any);
}}
/>
</Box>

View File

@@ -12,13 +12,13 @@ import {
Flex
} from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { useTranslation } from 'next-i18next';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../render/RenderInput';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type';
import RenderOutput from '../render/RenderOutput';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ExtractFieldModal, { defaultField } from './ExtractFieldModal';
@@ -42,7 +42,7 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
const CustomComponent = useMemo(
@@ -144,11 +144,10 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
return (
<NodeCard minW={'400px'} {...data}>
{toolInputs.length > 0 && (
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
@@ -174,8 +173,10 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
defaultField={editExtractFiled}
onClose={() => setEditExtractField(undefined)}
onSubmit={(data) => {
const extracts: ContextExtractAgentItemType[] =
inputs.find((item) => item.key === NodeInputKeyEnum.extractKeys)?.value || [];
const input = inputs.find(
(input) => input.key === NodeInputKeyEnum.extractKeys
) as FlowNodeInputItemType;
const extracts: ContextExtractAgentItemType[] = input.value || [];
const exists = extracts.find((item) => item.key === editExtractFiled.key);
@@ -188,7 +189,7 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
type: 'updateInput',
key: NodeInputKeyEnum.extractKeys,
value: {
...inputs.find((input) => input.key === NodeInputKeyEnum.extractKeys),
...input,
value: newInputs
}
});
@@ -203,7 +204,10 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
if (exists) {
if (editExtractFiled.key === data.key) {
const output = outputs.find((output) => output.key === data.key);
const output = outputs.find(
(output) => output.key === data.key
) as FlowNodeOutputItemType;
// update
onChangeNode({
nodeId,

View File

@@ -47,6 +47,8 @@ const CurlImportModal = ({
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
if (!requestUrl || !requestMethod || !params || !headers || !jsonBody) return;
const parsed = parse(content);
if (!parsed.url) {
throw new Error('url not found');

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../../components/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
@@ -106,8 +106,12 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod);
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const requestMethods = inputs.find(
(item) => item.key === NodeInputKeyEnum.httpMethod
) as FlowNodeInputItemType;
const requestUrl = inputs.find(
(item) => item.key === NodeInputKeyEnum.httpReqUrl
) as FlowNodeInputItemType;
const onChangeUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
onChangeNode({
@@ -251,6 +255,8 @@ export function RenderHttpProps({
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const getNodeDynamicInputs = useContextSelector(WorkflowContext, (v) => v.getNodeDynamicInputs);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value;
@@ -269,17 +275,10 @@ export function RenderHttpProps({
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit || input.toolDescription)
.map((item) => ({
key: item.key,
label: item.label
}))
);
const nodeVariables = formatEditorVariablePickerIcon(getNodeDynamicInputs(nodeId));
return [...moduleVariables, ...globalVariables];
}, [appDetail.chatConfig, inputs, nodeList, t]);
return [...nodeVariables, ...globalVariables];
}, [appDetail.chatConfig, getNodeDynamicInputs, nodeId, nodeList, t]);
const variableText = useMemo(() => {
return variables
@@ -637,7 +636,7 @@ const NodeHttp = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (v) => v.splitToolInputs);
const { toolInputs, commonInputs, isTool } = splitToolInputs(inputs, nodeId);
const { commonInputs, isTool } = splitToolInputs(inputs, nodeId);
const HttpMethodAndUrl = useMemoizedFn(() => (
<RenderHttpMethodAndUrl nodeId={nodeId} inputs={inputs} />
@@ -656,8 +655,7 @@ const NodeHttp = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}

View File

@@ -4,7 +4,7 @@ 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 { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';

View File

@@ -1,10 +1,10 @@
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import { Box, Button, Center, Flex, useDisclosure } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { getLafAppDetail } from '@/web/support/laf/api';
import MySelect from '@fastgpt/web/components/common/MySelect';
@@ -24,7 +24,7 @@ import RenderToolInput from './render/RenderToolInput';
import RenderInput from './render/RenderInput';
import RenderOutput from './render/RenderOutput';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
@@ -34,6 +34,7 @@ import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { putUpdateTeam } from '@/web/support/user/team/api';
import { nodeLafCustomInputConfig } from '@fastgpt/global/core/workflow/template/system/laf';
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
@@ -46,7 +47,9 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const requestUrl = inputs.find(
(item) => item.key === NodeInputKeyEnum.httpReqUrl
) as FlowNodeInputItemType;
const { userInfo, initUserInfo } = useUserStore();
@@ -122,8 +125,8 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
[lafFunctionSelectList, requestUrl?.value]
);
const { mutate: onSyncParams, isLoading: isSyncing } = useRequest({
mutationFn: async () => {
const { run: onSyncParams, loading: isSyncing } = useRequest2(
async () => {
await refetchFunction();
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
@@ -144,10 +147,10 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
// add input variables
const bodyParams =
lafFunction?.request?.content?.['application/json']?.schema?.properties || {};
const requiredParams =
lafFunction?.request?.content?.['application/json']?.schema?.required || [];
// add new params
const allParams = [
...Object.keys(bodyParams).map((key) => ({
name: key,
@@ -166,13 +169,11 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: param.required,
description: param.desc || '',
toolDescription: param.desc || '未设置参数描述',
canEdit: true,
editField: {
key: true,
valueType: true
}
toolDescription: param.desc || param.name,
customInputConfig: nodeLafCustomInputConfig,
canEdit: true
};
onChangeNode({
nodeId,
type: 'addInput',
@@ -211,8 +212,10 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
});
});
},
successToast: t('common.Sync success')
});
{
successToast: t('common.Sync success')
}
);
// not config laf
if (!token || !appid) {
@@ -299,19 +302,18 @@ const ConfigLaf = () => {
);
};
const RenderIO = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const RenderIO = ({ data }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { commonInputs, toolInputs, isTool } = splitToolInputs(inputs, nodeId);
const { commonInputs, isTool } = splitToolInputs(inputs, nodeId);
return (
<>
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}

View File

@@ -0,0 +1,446 @@
import React, { useCallback, useMemo } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea,
Stack,
HStack
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import dynamic from 'next/dynamic';
import { useI18n } from '@/web/context/I18n';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import { useBoolean } from 'ahooks';
const MyNumberInput = dynamic(
() => import('@fastgpt/web/components/common/Input/NumberInput/index')
);
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
export const defaultInput: FlowNodeInputItemType = {
renderTypeList: [FlowNodeInputTypeEnum.reference], // Can only choose one here
selectedTypeIndex: 0,
valueType: WorkflowIOValueTypeEnum.string,
key: '',
label: ''
};
const FieldEditModal = ({
defaultValue,
keys = [],
hasDynamicInput,
onClose,
onSubmit
}: {
defaultValue: FlowNodeInputItemType;
keys: string[];
hasDynamicInput: boolean;
onClose: () => void;
onSubmit: (e: { data: FlowNodeInputItemType; isChangeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { toast } = useToast();
const inputTypeList = useMemo(
() => [
{
label: t('core.workflow.inputType.Reference'),
value: FlowNodeInputTypeEnum.reference,
defaultValueType: WorkflowIOValueTypeEnum.string
},
{
label: t('core.workflow.inputType.input'),
value: FlowNodeInputTypeEnum.input,
defaultValueType: WorkflowIOValueTypeEnum.string
},
{
label: t('core.workflow.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
defaultValueType: WorkflowIOValueTypeEnum.string
},
{
label: t('core.workflow.inputType.JSON Editor'),
value: FlowNodeInputTypeEnum.JSONEditor,
defaultValueType: WorkflowIOValueTypeEnum.string
},
{
label: t('core.workflow.inputType.number input'),
value: FlowNodeInputTypeEnum.numberInput,
defaultValueType: WorkflowIOValueTypeEnum.number
},
{
label: t('core.workflow.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
defaultValueType: WorkflowIOValueTypeEnum.boolean
},
{
label: t('core.workflow.inputType.selectApp'),
value: FlowNodeInputTypeEnum.selectApp,
defaultValueType: WorkflowIOValueTypeEnum.selectApp
},
{
label: t('core.workflow.inputType.selectLLMModel'),
value: FlowNodeInputTypeEnum.selectLLMModel,
defaultValueType: WorkflowIOValueTypeEnum.string
},
{
label: t('core.workflow.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
defaultValueType: WorkflowIOValueTypeEnum.selectDataset
},
...(hasDynamicInput
? []
: [
{
label: t('core.workflow.inputType.dynamicTargetInput'),
value: FlowNodeInputTypeEnum.addInputParam,
defaultValueType: WorkflowIOValueTypeEnum.dynamic
}
])
],
[hasDynamicInput, t]
);
const isEdit = !!defaultValue.key;
const { register, getValues, setValue, handleSubmit, watch } = useForm({
defaultValues: defaultValue
});
const inputType = watch('renderTypeList.0') || FlowNodeInputTypeEnum.reference;
const valueType = watch('valueType');
const [isToolInput, { toggle: setIsToolInput }] = useBoolean(!!getValues('toolDescription'));
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
const showValueTypeSelect = inputType === FlowNodeInputTypeEnum.reference;
// input type config
const showRequired = useMemo(() => {
const list = [FlowNodeInputTypeEnum.addInputParam];
return !list.includes(inputType);
}, [inputType]);
const showDefaultValue = useMemo(() => {
const list = [
FlowNodeInputTypeEnum.input,
FlowNodeInputTypeEnum.textarea,
FlowNodeInputTypeEnum.JSONEditor,
FlowNodeInputTypeEnum.numberInput,
FlowNodeInputTypeEnum.switch
];
return list.includes(inputType);
}, [inputType]);
const showMaxLenInput = useMemo(() => {
const list = [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.textarea];
return list.includes(inputType);
}, [inputType]);
const showMinMaxInput = useMemo(() => {
const list = [FlowNodeInputTypeEnum.numberInput];
return list.includes(inputType);
}, [inputType]);
const valueTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({
label: t(item.label),
value: item.value
}));
const defaultValueType =
inputTypeList.find((item) => item.value === inputType)?.defaultValueType ||
WorkflowIOValueTypeEnum.string;
const onSubmitSuccess = useCallback(
(data: FlowNodeInputItemType) => {
data.key = data?.key?.trim();
if (!data.key) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Name Cannot Be Empty')
});
}
if (data.renderTypeList[0] !== FlowNodeInputTypeEnum.reference) {
data.valueType = defaultValueType;
}
const isChangeKey = defaultValue.key !== data.key;
// create check key
if (keys.includes(data.key)) {
if (!isEdit || isChangeKey) {
toast({
status: 'warning',
title: workflowT('Field Name already exists')
});
return;
}
}
if (isToolInput) {
data.toolDescription = data.description;
}
data.label = data.key;
onSubmit({
data,
isChangeKey
});
onClose();
},
[
defaultValue.key,
defaultValueType,
isEdit,
isToolInput,
keys,
onClose,
onSubmit,
t,
toast,
workflowT
]
);
const onSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? workflowT('Edit input') : workflowT('Add new input')}
maxW={['90vw', '800px']}
w={'100%'}
>
<ModalBody display={'flex'} gap={8} flexDirection={['column', 'row']}>
<Stack flex={1} gap={5}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1}>
<MySelect<FlowNodeInputTypeEnum>
list={inputTypeList}
value={inputType}
onchange={(e) => {
setValue('renderTypeList.0', e);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Field Name')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true
})}
/>
</Flex>
<Box alignItems={'flex-start'}>
<FormLabel flex={'0 0 70px'} mb={'1px'}>
{workflowT('Field description')}
</FormLabel>
<Textarea
bg={'myGray.50'}
placeholder={workflowT('Field description placeholder')}
rows={4}
{...register('description', { required: isToolInput ? true : false })}
/>
</Box>
</Stack>
{/* input type config */}
<Stack flex={1} gap={5}>
{/* value type */}
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
{showValueTypeSelect ? (
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
list={valueTypeSelectList}
value={valueType}
onchange={(e) => {
setValue('valueType', e);
}}
/>
</Box>
) : (
defaultValueType
)}
</Flex>
{showRequired && (
<Flex alignItems={'center'} minH={'40px'}>
<FormLabel flex={'1'}>{workflowT('Field required')}</FormLabel>
<Switch {...register('required')} />
</Flex>
)}
{/* reference */}
{inputType === FlowNodeInputTypeEnum.reference && (
<>
<Flex alignItems={'center'} minH={'40px'}>
<FormLabel flex={'1'}>{workflowT('Field used as tool input')}</FormLabel>
<Switch
isChecked={isToolInput}
onChange={(e) => {
setIsToolInput();
}}
/>
</Flex>
</>
)}
{showMaxLenInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Length')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
placeholder={t('core.module.Max Length placeholder')}
value={maxLength}
onChange={(e) => {
// @ts-ignore
setValue('maxLength', e || '');
}}
/>
</Flex>
)}
{showMinMaxInput && (
<>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('max')}
onChange={(e) => {
// @ts-ignore
setValue('max', e || '');
}}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Min Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('min')}
onChange={(e) => {
// @ts-ignore
setValue('min', e || '');
}}
/>
</Flex>
</>
)}
{showDefaultValue && (
<Flex alignItems={'center'} minH={'40px'}>
<FormLabel flex={inputType === FlowNodeInputTypeEnum.switch ? 1 : '0 0 70px'}>
{t('core.module.Default Value')}
</FormLabel>
{inputType === FlowNodeInputTypeEnum.numberInput && (
<Input
bg={'myGray.50'}
max={max}
min={min}
type={'number'}
{...register('defaultValue')}
/>
)}
{inputType === FlowNodeInputTypeEnum.input && (
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
)}
{inputType === FlowNodeInputTypeEnum.textarea && (
<Textarea bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
)}
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
<JsonEditor
bg={'myGray.50'}
resize
w={'full'}
onChange={(e) => {
setValue('defaultValue', e);
}}
defaultValue={String(getValues('defaultValue'))}
/>
)}
{inputType === FlowNodeInputTypeEnum.switch && (
<Switch {...register('defaultValue')} />
)}
</Flex>
)}
{inputType === FlowNodeInputTypeEnum.addInputParam && (
<>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1} fontWeight={'bold'}>
{workflowT('Only the Reference type is supported')}
</Box>
</Flex>
<Box>
<HStack mb={1}>
<FormLabel>{workflowT('Optional value type')}</FormLabel>
<QuestionTip label={workflowT('Optional value type tip')} />
</HStack>
<MultipleSelect<WorkflowIOValueTypeEnum>
list={valueTypeSelectList}
bg={'myGray.50'}
value={selectValueTypeList || []}
onSelect={(e) => {
setValue('customInputConfig.selectValueTypeList', e);
}}
/>
</Box>
</>
)}
</Stack>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);

View File

@@ -0,0 +1,169 @@
import React, { useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { Box, Button, HStack } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import Container from '../../components/Container';
import { useTranslation } from 'next-i18next';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import VariableTable from './VariableTable';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import IOTitle from '../../components/IOTitle';
import dynamic from 'next/dynamic';
import { defaultInput } from './InputEditModal';
const FieldEditModal = dynamic(() => import('./InputEditModal'));
/*
1. When the plug-in is called, the input of the rendering node is customized.
2. Customize input nodes. Input and output must be symmetrical.
3. When the plug-in is run, the external will calculate the value of the custom input and throw it to the output of the custom input node to start running the plug-in.
*/
const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs = [], outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onSubmit = ({ data }: { data: FlowNodeInputItemType; isChangeKey: boolean }) => {
if (!editField) return;
if (editField?.key) {
const output = outputs.find((output) => output.key === editField.key);
const newOutput: FlowNodeOutputItemType = {
...(output as FlowNodeOutputItemType),
valueType: data.valueType,
key: data.key,
label: data.label
};
onChangeNode({
nodeId,
type: 'replaceInput',
key: editField.key,
value: data
});
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
const newOutput: FlowNodeOutputItemType = {
id: data.key,
valueType: data.valueType,
key: data.key,
label: data.label,
type: FlowNodeOutputTypeEnum.hidden
};
// Add new input
onChangeNode({
nodeId,
type: 'addInput',
value: data
});
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
}
};
const Render = useMemo(() => {
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<HStack className="nodrag" cursor={'default'} mb={3}>
<IOTitle text={t('core.workflow.Custom inputs')} mb={0} />
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setEditField(defaultInput)}
>
{t('common.Add New')}
</Button>
</HStack>
<VariableTable
variables={inputs.map((input) => {
const inputType = input.renderTypeList[0];
return {
icon: FlowNodeInputMap[inputType]?.icon as string,
label: t(input.label),
type: input.valueType ? t(FlowValueTypeMap[input.valueType]?.label) : '-',
isTool: !!input.toolDescription,
key: input.key
};
})}
onEdit={(key) => {
const input = inputs.find((input) => input.key === key);
if (!input) return;
setEditField(input);
}}
onDelete={(key) => {
onChangeNode({
nodeId,
type: 'delInput',
key
});
onChangeNode({
nodeId,
type: 'delOutput',
key
});
}}
/>
</Container>
</NodeCard>
);
}, [data, inputs, nodeId, onChangeNode, selected, t]);
return (
<>
{Render}
{!!editField && (
<FieldEditModal
defaultValue={editField}
keys={inputs.map((item) => item.key)}
hasDynamicInput={
!!inputs.find(
(input) =>
input.key !== editField.key &&
input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam)
)
}
onClose={() => setEditField(undefined)}
onSubmit={onSubmit}
/>
)}
</>
);
};
export default React.memo(NodePluginInput);

View File

@@ -0,0 +1,221 @@
import React, { useCallback, useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import Container from '../../components/Container';
import { FlowNodeInputItemType, ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { VARIABLE_NODE_ID, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import RenderInput from '../render/RenderInput';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import IOTitle from '../../components/IOTitle';
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ValueTypeLabel from '../render/ValueTypeLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useI18n } from '@/web/context/I18n';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import { defaultInput } from '../render/RenderInput/FieldEditModal';
const FieldEditModal = dynamic(() => import('../render/RenderInput/FieldEditModal'));
const customInputConfig = {
selectValueTypeList: Object.values(WorkflowIOValueTypeEnum),
showDescription: true,
showDefaultValue: true
};
const NodePluginOutput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
debug: true,
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<IOTitle mb={0} text={t('core.workflow.Custom outputs')}></IOTitle>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setEditField(defaultInput)}
>
{t('common.Add New')}
</Button>
</Flex>
{/* render input */}
<Box mt={2}>
{inputs.map((input) => (
<Box key={input.key} _notLast={{ mb: 3 }}>
<Reference nodeId={nodeId} keys={inputs.map((input) => input.key)} input={input} />
</Box>
))}
</Box>
</Container>
{!!editField && (
<FieldEditModal
customInputConfig={customInputConfig}
defaultInput={editField}
keys={inputs.map((input) => input.key)}
onClose={() => setEditField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
nodeId,
type: 'addInput',
value: data
});
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginOutput);
function Reference({
nodeId,
keys,
input
}: {
nodeId: string;
keys: string[];
input: FlowNodeInputItemType;
}) {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { ConfirmModal, openConfirm } = useConfirm({
type: 'delete',
content: workflowT('Confirm delete field tip')
});
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
const value =
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e;
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
value
}
});
},
[input, nodeId, nodeList, onChangeNode]
);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: input.valueType,
value: input.value
});
const onUpdateField = useCallback(
({ data }: { data: FlowNodeInputItemType }) => {
if (!data.key) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: input.key,
value: data
});
},
[input.key, nodeId, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: input.key
});
}, [input.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={input.required}>{input.label}</FormLabel>
{input.description && <QuestionTip label={input.description}></QuestionTip>}
{/* value */}
<ValueTypeLabel valueType={input.valueType} />
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() => setEditField(input)}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={openConfirm(onDel)}
/>
</Flex>
<ReferSelector
placeholder={t(input.referencePlaceholder || '选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
{!!editField && (
<FieldEditModal
defaultInput={editField}
customInputConfig={customInputConfig}
keys={keys}
onClose={() => setEditField(undefined)}
onSubmit={onUpdateField}
/>
)}
<ConfirmModal />
</>
);
}

View File

@@ -0,0 +1,75 @@
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
const VariableTable = ({
variables = [],
onEdit,
onDelete
}: {
variables: { icon?: string; label: string; type: string; key: string; isTool?: boolean }[];
onEdit: (key: string) => void;
onDelete: (key: string) => void;
}) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const showToolColumn = variables.some((item) => item.isTool);
return (
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('core.module.variable.variable name')}
</Th>
<Th>{t('core.workflow.Value type')}</Th>
{showToolColumn && <Th>{workflowT('Tool input')}</Th>}
<Th borderBottomRightRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item) => (
<Tr key={item.key}>
<Td>
<Flex alignItems={'center'}>
{!!item.icon && <MyIcon name={item.icon as any} w={'14px'} mr={1} />}
{item.label || item.key}
</Flex>
</Td>
<Td>{item.type}</Td>
{showToolColumn && <Th>{item.isTool ? '✅' : '-'}</Th>}
<Td>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => onEdit(item.key)}
/>
<MyIcon
className="delete"
name={'delete'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onDelete(item.key);
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
);
};
export default React.memo(VariableTable);

View File

@@ -1,225 +0,0 @@
import React, { useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import Container from '../components/Container';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type.d';
import { useTranslation } from 'next-i18next';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import VariableTable from './render/VariableTable';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
required: true
};
const createEditField: EditInputFieldMapType = {
key: true,
description: true,
required: true,
valueType: true,
inputType: true
};
const dynamicInputEditField: EditInputFieldMapType = {
key: true
};
const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
const Render = useMemo(() => {
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} mb={3}>
<Box>{t('core.workflow.Custom inputs')}</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setCreateField(defaultCreateField)}
>
{t('common.Add New')}
</Button>
</Flex>
<VariableTable
fieldEditType={createEditField}
keys={inputs.map((input) => input.key)}
onCloseFieldEdit={() => {
setCreateField(undefined);
setEditField(undefined);
}}
variables={inputs.map((input) => {
const inputType = input.renderTypeList[0];
return {
icon: FlowNodeInputMap[inputType]?.icon as string,
label: t(input.label),
type: input.valueType ? t(FlowValueTypeMap[input.valueType]?.label) : '-',
key: input.key
};
})}
createField={createField}
onCreate={({ data }) => {
if (!data.key || !data.inputType) {
return;
}
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label || '',
renderTypeList: [data.inputType],
required: data.required,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
canEdit: true,
value: data.defaultValue,
editField: dynamicInputEditField,
maxLength: data.maxLength,
max: data.max,
min: data.min,
dynamicParamDefaultValue: data.dynamicParamDefaultValue
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.static
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
setCreateField(undefined);
}}
editField={editField}
onStartEdit={(key) => {
const input = inputs.find((input) => input.key === key);
if (!input) return;
setEditField({
...input,
inputType: input.renderTypeList[0],
isToolInput: !!input.toolDescription
});
}}
onEdit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !editField?.key) return;
const output = outputs.find((output) => output.key === editField.key);
const newInput: FlowNodeInputItemType = {
...data,
key: data.key,
label: data.label || '',
renderTypeList: [data.inputType],
toolDescription: data.isToolInput ? data.description : undefined,
canEdit: true,
value: data.defaultValue,
editField: dynamicInputEditField
};
const newOutput: FlowNodeOutputItemType = {
...(output as FlowNodeOutputItemType),
valueType: data.valueType,
key: data.key,
label: data.label
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
nodeId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
onDelete={(key) => {
onChangeNode({
nodeId,
type: 'delInput',
key
});
onChangeNode({
nodeId,
type: 'delOutput',
key
});
}}
/>
</Container>
</NodeCard>
);
}, [createField, data, editField, inputs, nodeId, onChangeNode, outputs, selected, t]);
return Render;
};
export default React.memo(NodePluginInput);

View File

@@ -1,105 +0,0 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import { EditInputFieldMapType, EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import RenderInput from './render/RenderInput';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const FieldEditModal = dynamic(() => import('./render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
inputType: FlowNodeInputTypeEnum.reference,
key: '',
description: '',
valueType: WorkflowIOValueTypeEnum.string
};
const createEditField: EditInputFieldMapType = {
key: true,
description: true,
valueType: true
};
const NodePluginOutput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [createField, setCreateField] = useState<EditNodeFieldType>();
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
debug: true,
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} fontWeight={'medium'}>
{t('core.workflow.Custom outputs')}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setCreateField(defaultCreateField)}
>
{t('common.Add New')}
</Button>
</Flex>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Container>
{!!createField && (
<FieldEditModal
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
if (!data.key || !data.label) {
return;
}
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: false,
description: data.description,
canEdit: true,
editField: createEditField
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
setCreateField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginOutput);

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import RenderOutput from './render/RenderOutput';
@@ -21,17 +21,16 @@ const NodeSimple = ({
const { t } = useTranslation();
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { nodeId, inputs, outputs } = data;
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const filterHiddenInputs = useMemo(() => commonInputs.filter((item) => true), [commonInputs]);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{toolInputs.length > 0 && (
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}

View File

@@ -1,7 +1,7 @@
import React, { Dispatch, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import { Box, useTheme } from '@chakra-ui/react';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Divider from '../components/Divider';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from './render/NodeCard';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useTranslation } from 'next-i18next';
import {
Box,

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';

View File

@@ -1,530 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea,
Stack
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import dynamic from 'next/dynamic';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput/index';
import { useI18n } from '@/web/context/I18n';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const EmptyTip = dynamic(() => import('@fastgpt/web/components/common/EmptyTip'));
const defaultValue: EditNodeFieldType = {
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
key: '',
label: '',
description: '',
isToolInput: false,
defaultValue: '',
maxLength: undefined,
max: undefined,
min: undefined,
editField: {},
dynamicParamDefaultValue: {
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
required: true
}
};
const FieldEditModal = ({
editField = {
key: true
},
defaultField,
keys = [],
onClose,
onSubmit
}: {
editField?: EditInputFieldMapType;
defaultField: EditNodeFieldType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { toast } = useToast();
const showDynamicInputSelect =
!keys.includes(NodeInputKeyEnum.addInputParam) ||
defaultField.key === NodeInputKeyEnum.addInputParam;
const inputTypeList = useMemo(
() => [
{
label: t('core.workflow.inputType.Reference'),
value: FlowNodeInputTypeEnum.reference,
defaultValue: {}
},
{
label: t('core.workflow.inputType.input'),
value: FlowNodeInputTypeEnum.input,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.JSON Editor'),
value: FlowNodeInputTypeEnum.JSONEditor,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.number input'),
value: FlowNodeInputTypeEnum.numberInput,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.number
}
},
{
label: t('core.workflow.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.boolean
}
},
{
label: t('core.workflow.inputType.selectApp'),
value: FlowNodeInputTypeEnum.selectApp,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.selectApp
}
},
{
label: t('core.workflow.inputType.selectLLMModel'),
value: FlowNodeInputTypeEnum.selectLLMModel,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.selectDataset
}
},
...(showDynamicInputSelect
? [
{
label: t('core.workflow.inputType.dynamicTargetInput'),
value: FlowNodeInputTypeEnum.addInputParam,
defaultValue: {
label: t('core.workflow.inputType.dynamicTargetInput'),
valueType: WorkflowIOValueTypeEnum.dynamic,
key: NodeInputKeyEnum.addInputParam,
required: false
}
}
]
: [])
],
[showDynamicInputSelect, t]
);
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: {
...defaultValue,
...defaultField,
valueType: defaultField.valueType ?? WorkflowIOValueTypeEnum.string
}
});
const inputType = watch('inputType');
const valueType = watch('valueType');
const isToolInput = watch('isToolInput');
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const defaultInputValueType = watch('dynamicParamDefaultValue.valueType');
const showKeyInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.key;
}, [editField.key, inputType]);
const showInputTypeSelect = useMemo(() => {
return editField.inputType;
}, [editField.inputType]);
const showDescriptionInput = useMemo(() => {
return editField.description;
}, [editField.description]);
const showValueTypeSelect = useMemo(() => {
if (!editField.valueType) return false;
if (inputType !== FlowNodeInputTypeEnum.reference) return false;
return true;
}, [editField.valueType, inputType]);
// input type config
const showToolInput = useMemo(() => {
return inputType === FlowNodeInputTypeEnum.reference;
}, [inputType]);
const showDefaultValue = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.input) return true;
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
if (inputType === FlowNodeInputTypeEnum.JSONEditor) return true;
if (inputType === FlowNodeInputTypeEnum.numberInput) return true;
if (inputType === FlowNodeInputTypeEnum.switch) return true;
return false;
}, [inputType]);
const showMaxLenInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.input) return true;
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
return false;
}, [inputType]);
const showMinMaxInput = useMemo(
() => inputType === FlowNodeInputTypeEnum.numberInput,
[inputType]
);
const showDynamicInput = useMemo(() => {
return inputType === FlowNodeInputTypeEnum.addInputParam;
}, [inputType]);
const slicedTypeMap = Object.values(FlowValueTypeMap).slice(0, -1);
const dataTypeSelectList = slicedTypeMap.map((item) => ({
label: t(item.label),
value: item.value
}));
const onSubmitSuccess = useCallback(
(data: EditNodeFieldType) => {
data.key = data?.key?.trim();
// add default value
const inputTypeConfig = inputTypeList.find((item) => item.value === data.inputType);
if (inputTypeConfig?.defaultValue) {
data.label = data.key;
for (const key in inputTypeConfig.defaultValue) {
// @ts-ignore
data[key] = inputTypeConfig.defaultValue[key];
}
}
if (!data.key) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Name Cannot Be Empty')
});
}
// create check key
if (!defaultField.key && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
// edit check repeat key
if (defaultField.key && defaultField.key !== data.key && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
if (showValueTypeSelect && !data.valueType) {
return toast({
status: 'warning',
title: '数据类型不能为空'
});
}
onSubmit({
data,
changeKey: !keys.includes(data.key)
});
},
[defaultField.key, inputTypeList, keys, onSubmit, showValueTypeSelect, t, toast]
);
const onSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={t('core.module.edit.Field Edit')}
maxW={['90vw', showInputTypeSelect ? '800px' : '400px']}
w={'100%'}
overflow={'unset'}
>
<ModalBody overflow={'visible'}>
<Flex gap={8} flexDirection={['column', 'row']}>
<Stack flex={1} gap={5}>
{showInputTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1}>
<MySelect
list={inputTypeList}
value={inputType}
onchange={(e: string) => {
const type = e as FlowNodeInputTypeEnum;
setValue('inputType', type);
}}
/>
</Box>
</Flex>
)}
{showValueTypeSelect && !showInputTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
w={'full'}
list={dataTypeSelectList}
value={valueType}
onchange={(e: string) => {
const type = e as WorkflowIOValueTypeEnum;
setValue('valueType', type);
}}
/>
</Box>
</Flex>
)}
{showKeyInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Field Name')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true
})}
/>
</Flex>
)}
{showDescriptionInput && (
<Box alignItems={'flex-start'}>
<FormLabel flex={'0 0 70px'} mb={'1px'}>
{t('core.module.Field Description')}
</FormLabel>
<Textarea
bg={'myGray.50'}
placeholder={
isToolInput ? t('core.module.Plugin tool Description') : t('common.choosable')
}
rows={5}
{...register('description', { required: isToolInput ? true : false })}
/>
</Box>
)}
</Stack>
{/* input type config */}
{showInputTypeSelect && (
<Stack flex={1} gap={5}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('Field required')}</FormLabel>
<Switch {...register('required')} />
</Flex>
{showToolInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}></FormLabel>
<Switch {...register('isToolInput')} />
</Flex>
)}
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
w={'full'}
list={dataTypeSelectList}
value={valueType}
onchange={(e: string) => {
const type = e as WorkflowIOValueTypeEnum;
setValue('valueType', type);
}}
/>
</Box>
</Flex>
)}
{showDefaultValue && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Default Value')}</FormLabel>
{inputType === FlowNodeInputTypeEnum.numberInput && (
<Input
bg={'myGray.50'}
max={max}
min={min}
type={'number'}
{...register('defaultValue')}
/>
)}
{inputType === FlowNodeInputTypeEnum.input && (
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
)}
{inputType === FlowNodeInputTypeEnum.textarea && (
<Textarea
bg={'myGray.50'}
maxLength={maxLength}
{...register('defaultValue')}
/>
)}
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
<JsonEditor
resize
w={'full'}
onChange={(e) => {
setValue('defaultValue', e);
}}
defaultValue={String(getValues('defaultValue'))}
/>
)}
{inputType === FlowNodeInputTypeEnum.switch && (
<Switch {...register('defaultValue')} />
)}
</Flex>
)}
{showMaxLenInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Length')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
placeholder={t('core.module.Max Length placeholder')}
value={maxLength}
onChange={(e) => {
// @ts-ignore
setValue('maxLength', e);
}}
// {...register('maxLength')}
/>
</Flex>
)}
{showMinMaxInput && (
<>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('max')}
onChange={(e) => {
// @ts-ignore
setValue('max', e);
}}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Min Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('min')}
onChange={(e) => {
// @ts-ignore
setValue('min', e);
}}
/>
</Flex>
</>
)}
{showDynamicInput && (
<Stack gap={5}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1} fontWeight={'bold'}>
{t('core.workflow.inputType.Reference')}
</Box>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
list={dataTypeSelectList}
value={defaultInputValueType}
onchange={(e) => {
setValue(
'dynamicParamDefaultValue.valueType',
e as WorkflowIOValueTypeEnum
);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.workflow.inputType.Required')}</FormLabel>
<Box flex={1}>
<Switch {...register('dynamicParamDefaultValue.required')} />
</Box>
</Flex>
</Stack>
)}
{!showToolInput &&
!showValueTypeSelect &&
!showDefaultValue &&
!showMaxLenInput &&
!showMinMaxInput &&
!showDynamicInput && <EmptyTip text={t('core.module.No Config Tips')} />}
</Stack>
)}
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box, Button, Card, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { useTranslation } from 'next-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -111,7 +111,7 @@ const NodeCard = (props: Props) => {
const onClickSyncVersion = useCallback(async () => {
try {
if (!node || !template) return;
if (node?.flowNodeType === 'pluginModule') {
if (node?.flowNodeType === FlowNodeTypeEnum.pluginModule) {
if (!node.pluginId) return;
onResetNode({
id: nodeId,

View File

@@ -0,0 +1,190 @@
import { useI18n } from '@/web/context/I18n';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import {
Box,
Button,
Flex,
Input,
ModalBody,
ModalFooter,
Stack,
Textarea
} from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
CustomFieldConfigType,
FlowNodeInputItemType
} from '@fastgpt/global/core/workflow/type/io';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useToast } from '@fastgpt/web/hooks/useToast';
import React, { useCallback, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { useMount } from 'ahooks';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context';
const FieldModal = ({
customInputConfig,
defaultInput,
keys,
onClose,
onSubmit
}: {
customInputConfig: CustomFieldConfigType;
defaultInput: FlowNodeInputItemType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: FlowNodeInputItemType; isChangeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT, commonT } = useI18n();
const { toast } = useToast();
const isEdit = !!defaultInput.key;
const { register, setValue, handleSubmit, watch } = useForm<FlowNodeInputItemType>({
defaultValues: defaultInput
});
const inputType = FlowNodeInputTypeEnum.reference;
// value type select
const showValueTypeSelect = useMemo(() => {
if (!customInputConfig.selectValueTypeList || customInputConfig.selectValueTypeList.length <= 1)
return false;
if (inputType === FlowNodeInputTypeEnum.reference) return true;
return false;
}, [customInputConfig.selectValueTypeList, inputType]);
const valueTypeSelectLit = useMemo(() => {
if (!customInputConfig.selectValueTypeList) return [];
const dataTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({
label: t(item.label),
value: item.value
}));
return dataTypeSelectList.filter((item) =>
customInputConfig.selectValueTypeList?.includes(item.value)
);
}, [customInputConfig.selectValueTypeList, t]);
const valueType = watch('valueType');
useMount(() => {
if (
customInputConfig.selectValueTypeList &&
customInputConfig.selectValueTypeList.length > 0 &&
!valueType
) {
setValue('valueType', customInputConfig.selectValueTypeList[0]);
}
});
const onSubmitSuccess = useCallback(
(data: FlowNodeInputItemType) => {
const isChangeKey = defaultInput.key !== data.key;
if (keys.includes(data.key)) {
// 只要编辑状态且未改变key就不提示
if (!isEdit || isChangeKey) {
toast({
status: 'warning',
title: workflowT('Field Name already exists')
});
return;
}
}
data.key = data?.key?.trim();
data.label = data.key;
onSubmit({
data,
isChangeKey
});
onClose();
},
[defaultInput.key, isEdit, keys, onClose, onSubmit, toast, workflowT]
);
const onSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? workflowT('Edit input') : workflowT('Add new input')}
overflow={'unset'}
>
<ModalBody w={'100%'} overflow={'auto'} display={'flex'} flexDirection={['column', 'row']}>
<Stack w={'100%'} spacing={3}>
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{commonT('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
w={'full'}
list={valueTypeSelectLit}
value={valueType}
onchange={(e) => {
setValue('valueType', e);
}}
/>
</Box>
</Flex>
)}
{/* key */}
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'} required>
{t('core.module.Field Name')}
</FormLabel>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true
})}
/>
</Flex>
{customInputConfig.showDescription && (
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('Input description')}</FormLabel>
<Textarea bg={'myGray.50'} {...register('description', {})} />
</Flex>
)}
</Stack>
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteBase'} onClick={onClose}>
{commonT('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{commonT('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default FieldModal;
export const defaultInput: FlowNodeInputItemType = {
renderTypeList: [FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.string,
canEdit: true,
key: '',
label: ''
};

View File

@@ -1,20 +1,15 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import NodeInputSelect from '@fastgpt/web/components/core/workflow/NodeInputSelect';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import ValueTypeLabel from '../ValueTypeLabel';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
type Props = {
nodeId: string;
@@ -26,18 +21,7 @@ const InputLabel = ({ nodeId, input }: Props) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const {
description,
toolDescription,
required,
label,
selectedTypeIndex,
renderTypeList,
valueType,
canEdit,
key
} = input;
const [editField, setEditField] = useState<EditNodeFieldType>();
const { description, required, label, selectedTypeIndex, renderTypeList, valueType } = input;
const onChangeRenderType = useCallback(
(e: string) => {
@@ -68,63 +52,12 @@ const InputLabel = ({ nodeId, input }: Props) => {
fontWeight={'medium'}
color={'myGray.600'}
>
{required && (
<Box position={'absolute'} left={-2} top={-1} color={'red.600'}>
*
</Box>
)}
{t(label)}
<FormLabel required={required}>{t(label)}</FormLabel>
{description && <QuestionTip ml={1} label={t(description)}></QuestionTip>}
</Flex>
{/* value type */}
{renderType === FlowNodeInputTypeEnum.reference && <ValueTypeLabel valueType={valueType} />}
{/* edit config */}
{canEdit && (
<>
{input.editField && Object.keys(input.editField).length > 0 && (
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
...input,
inputType: renderTypeList[0],
valueType: valueType,
key,
label,
description,
isToolInput: !!toolDescription
})
}
/>
)}
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.600'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'delInput',
key: key
});
onChangeNode({
nodeId,
type: 'delOutput',
key: key
});
}}
/>
</>
)}
{/* input type select */}
{renderTypeList && renderTypeList.length > 1 && (
<Box ml={2}>
@@ -135,67 +68,16 @@ const InputLabel = ({ nodeId, input }: Props) => {
/>
</Box>
)}
{!!editField?.key && (
<FieldEditModal
editField={input.editField}
keys={[editField.key]}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label || !editField.key) return;
const newInput: FlowNodeInputItemType = {
...input,
renderTypeList: [data.inputType],
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
maxLength: data.maxLength,
value: data.defaultValue,
max: data.max,
min: data.min
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
}, [
canEdit,
description,
editField,
input,
key,
label,
nodeId,
onChangeNode,
onChangeRenderType,
renderTypeList,
required,
selectedTypeIndex,
t,
toolDescription,
valueType
]);

View File

@@ -53,7 +53,7 @@ const RenderList: {
},
{
types: [FlowNodeInputTypeEnum.addInputParam],
Component: dynamic(() => import('./templates/AddInputParam'))
Component: dynamic(() => import('./templates/DynamicInputs/index'))
},
{
types: [FlowNodeInputTypeEnum.JSONEditor],
@@ -76,10 +76,7 @@ type Props = {
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
const copyInputs = useMemo(() => JSON.stringify(flowInputList), [flowInputList]);
const filterInputs = useMemo(() => {
const parseSortInputs = JSON.parse(copyInputs) as FlowNodeInputItemType[];
return parseSortInputs.filter((input) => {
return true;
});
return JSON.parse(copyInputs) as FlowNodeInputItemType[];
}, [copyInputs]);
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]);
@@ -87,6 +84,8 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
const Render = useMemo(() => {
return filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const isDynamic = !!input.canEdit;
const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
return <>{memoCustomComponent[input.key]({ ...input })}</>;
@@ -98,7 +97,7 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
})();
return renderType !== FlowNodeInputTypeEnum.hidden ? (
return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? (
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
{!!input.label && !hideLabelTypeList.includes(renderType) && (
<InputLabel nodeId={nodeId} input={input} />

View File

@@ -1,114 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import Reference from './Reference';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { AppContext } from '@/pages/app/detail/components/context';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddInputParam = (props: RenderInputProps) => {
const { item, inputs, nodeId } = props;
const { t } = useTranslation();
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const inputValue = useMemo(() => (item.value || []) as FlowNodeInputItemType[], [item.value]);
const [editField, setEditField] = useState<EditNodeFieldType>();
const inputIndex = useMemo(
() => inputs?.findIndex((input) => input.key === item.key),
[inputs, item.key]
);
const onAddField = useCallback(
({ data }: { data: EditNodeFieldType }) => {
if (!data.key) return;
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label || '',
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: data.required,
description: data.description,
canEdit: true,
editField: item.editField
};
onChangeNode({
nodeId,
type: 'addInput',
index: inputIndex ? inputIndex + 1 : 1,
value: newInput
});
setEditField(undefined);
},
[inputIndex, item, nodeId, onChangeNode]
);
const Render = useMemo(() => {
return (
<>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Flex
alignItems={'center'}
position={'relative'}
fontWeight={'medium'}
color={'myGray.600'}
>
{t('core.workflow.Custom variable')}
{item.description && <QuestionTip ml={1} label={t(item.description)} />}
</Flex>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setEditField(item.dynamicParamDefaultValue ?? {})}
>
{t('common.Add New')}
</Button>
</Flex>
{appDetail.type === AppTypeEnum.plugin && (
<Box mt={1}>
<Reference {...props} />
</Box>
)}
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={inputValue.map((input) => input.key)}
onClose={() => setEditField(undefined)}
onSubmit={onAddField}
/>
)}
</>
);
}, [
appDetail.type,
editField,
inputValue,
item.description,
item.dynamicParamDefaultValue,
item.editField,
onAddField,
props,
t
]);
return Render;
};
export default React.memo(AddInputParam);

View File

@@ -0,0 +1,229 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { RenderInputProps } from '../../type';
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType, ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { defaultInput } from '../../FieldEditModal';
import { getInputComponentProps } from '@fastgpt/global/core/workflow/node/io/utils';
import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { ReferSelector, useReference } from '../Reference';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import ValueTypeLabel from '../../../ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const DynamicInputs = (props: RenderInputProps) => {
const { item, inputs = [], nodeId } = props;
const { t } = useTranslation();
const { workflowT } = useI18n();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const dynamicInputs = useMemo(() => inputs.filter((item) => item.canEdit), [inputs]);
const keys = useMemo(() => {
return inputs.map((input) => input.key);
}, [inputs]);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onAddField = useCallback(
({ data }: { data: FlowNodeInputItemType }) => {
if (!data.key) return;
const newInput: FlowNodeInputItemType = {
...data,
required: true
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
},
[nodeId, onChangeNode]
);
const Render = useMemo(() => {
return (
<Box borderBottom={'base'} pb={3}>
<HStack className="nodrag" cursor={'default'} position={'relative'}>
<HStack spacing={1} position={'relative'} fontWeight={'medium'} color={'myGray.600'}>
<Box>{item.label || workflowT('Custom input')}</Box>
{item.description && <QuestionTip label={t(item.description)} />}
</HStack>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() =>
setEditField({
...defaultInput,
...getInputComponentProps(item)
})
}
>
{t('common.Add New')}
</Button>
</HStack>
{/* field render */}
<Box mt={2}>
{dynamicInputs.map((children) => (
<Box key={children.key} _notLast={{ mb: 3 }}>
<Reference {...props} inputChildren={children} />
</Box>
))}
</Box>
{!!editField && !!item.customInputConfig && (
<FieldEditModal
defaultInput={editField}
customInputConfig={item.customInputConfig}
keys={keys}
onClose={() => setEditField(undefined)}
onSubmit={onAddField}
/>
)}
</Box>
);
}, [editField, dynamicInputs, item, keys, onAddField, props, t, workflowT]);
return Render;
};
export default React.memo(DynamicInputs);
function Reference({
inputChildren,
...props
}: RenderInputProps & {
inputChildren: FlowNodeInputItemType;
}) {
const { nodeId, inputs = [], item } = props;
const { t } = useTranslation();
const { workflowT } = useI18n();
const { ConfirmModal, openConfirm } = useConfirm({
type: 'delete',
content: workflowT('Confirm delete field tip')
});
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const keys = useMemo(() => {
return inputs.map((input) => input.key);
}, [inputs]);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
const newValue =
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e;
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value: newValue
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
const onUpdateField = useCallback(
({ data }: { data: FlowNodeInputItemType }) => {
if (!data.key) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: data
});
},
[inputChildren.key, nodeId, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{inputChildren.label}</FormLabel>
{inputChildren.description && (
<QuestionTip ml={1} label={inputChildren.description}></QuestionTip>
)}
{/* value */}
<ValueTypeLabel valueType={inputChildren.valueType} />
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() => setEditField(inputChildren)}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={openConfirm(onDel)}
/>
</Flex>
<ReferSelector
placeholder={t(inputChildren.referencePlaceholder || '选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
{!!editField && !!item.customInputConfig && (
<FieldEditModal
defaultInput={editField}
customInputConfig={item.customInputConfig}
keys={keys}
onClose={() => setEditField(undefined)}
onSubmit={onUpdateField}
/>
)}
<ConfirmModal />
</>
);
}

View File

@@ -13,6 +13,8 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const getNodeDynamicInputs = useContextSelector(WorkflowContext, (v) => v.getNodeDynamicInputs);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable
@@ -23,16 +25,9 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
const nodeVariables = formatEditorVariablePickerIcon(getNodeDynamicInputs(nodeId));
return [...globalVariables, ...moduleVariables];
return [...globalVariables, ...nodeVariables];
}, [inputs, nodeList]);
const update = useCallback(

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/runApp/type';
import Avatar from '@/components/Avatar';
import SelectAppModal from '../../../../SelectAppModal';
import { useTranslation } from 'next-i18next';

View File

@@ -13,6 +13,8 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const getNodeDynamicInputs = useContextSelector(WorkflowContext, (v) => v.getNodeDynamicInputs);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable
@@ -23,16 +25,14 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit)
.map((item) => ({
key: item.key,
label: item.label
}))
const nodeVariables = formatEditorVariablePickerIcon(
getNodeDynamicInputs(nodeId).map((item) => ({
key: item.key,
label: item.label
}))
);
return [...globalVariables, ...moduleVariables];
return [...globalVariables, ...nodeVariables];
}, [nodeList, inputs, t]);
const onChange = useCallback(
@@ -49,6 +49,7 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
},
[item, nodeId, onChangeNode]
);
const Render = useMemo(() => {
return (
<PromptEditor

View File

@@ -0,0 +1,185 @@
import { useI18n } from '@/web/context/I18n';
import {
FlowNodeOutputTypeEnum,
FlowValueTypeMap
} from '@fastgpt/global/core/workflow/node/constant';
import {
Box,
Button,
Flex,
Input,
ModalBody,
ModalFooter,
Stack,
Textarea
} from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
CustomFieldConfigType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useToast } from '@fastgpt/web/hooks/useToast';
import React, { useCallback, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { useMount } from 'ahooks';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { getNanoid } from '@fastgpt/global/common/string/tools';
const FieldModal = ({
customFieldConfig,
defaultValue,
keys,
onClose,
onSubmit
}: {
customFieldConfig: CustomFieldConfigType;
defaultValue: FlowNodeOutputItemType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: FlowNodeOutputItemType; isChangeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT, commonT } = useI18n();
const { toast } = useToast();
const isEdit = !!defaultValue.key;
const { register, setValue, handleSubmit, watch } = useForm<FlowNodeOutputItemType>({
defaultValues: defaultValue
});
const valueType = watch('valueType');
// value type select
const showValueTypeSelect = useMemo(() => {
if (!customFieldConfig.selectValueTypeList || customFieldConfig.selectValueTypeList.length <= 1)
return false;
return true;
}, [customFieldConfig.selectValueTypeList]);
const valueTypeSelectLit = useMemo(() => {
if (!customFieldConfig.selectValueTypeList) return [];
const dataTypeSelectList = Object.values(FlowValueTypeMap)
.slice(0, -1)
.map((item) => ({
label: t(item.label),
value: item.value
}));
return dataTypeSelectList.filter((item) =>
customFieldConfig.selectValueTypeList?.includes(item.value)
);
}, [customFieldConfig.selectValueTypeList, t]);
const onSubmitSuccess = useCallback(
(data: FlowNodeOutputItemType) => {
const isChangeKey = defaultValue.key !== data.key;
if (keys.includes(data.key)) {
// 只要编辑状态且未改变key就不提示
if (!isEdit || isChangeKey) {
toast({
status: 'warning',
title: workflowT('Field Name already exists')
});
return;
}
}
data.id = data.id || getNanoid();
data.key = data?.key?.trim();
data.label = data.key;
onSubmit({
data,
isChangeKey
});
onClose();
},
[defaultValue.key, isEdit, keys, onClose, onSubmit, toast, workflowT]
);
const onSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? workflowT('Edit input') : workflowT('Add new input')}
overflow={'unset'}
>
<ModalBody w={'100%'} overflow={'auto'} display={'flex'} flexDirection={['column', 'row']}>
<Stack w={'100%'} spacing={3}>
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{commonT('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
w={'full'}
list={valueTypeSelectLit}
value={valueType}
onchange={(e) => {
setValue('valueType', e);
}}
/>
</Box>
</Flex>
)}
{/* key */}
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'} required>
{t('core.module.Field Name')}
</FormLabel>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true
})}
/>
</Flex>
{customFieldConfig.showDescription && (
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('Input description')}</FormLabel>
<Textarea bg={'myGray.50'} {...register('description', {})} />
</Flex>
)}
</Stack>
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteBase'} onClick={onClose}>
{commonT('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{commonT('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default FieldModal;
export const defaultOutput: FlowNodeOutputItemType = {
id: '',
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.dynamic,
key: '',
label: ''
};

View File

@@ -7,13 +7,16 @@ import OutputLabel from './Label';
import { RenderOutputProps } from './type';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import VariableTable from '../VariableTable';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import VariableTable from '../../NodePluginIO/VariableTable';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import dynamic from 'next/dynamic';
import { defaultOutput } from './FieldEditModal';
const FieldEditModal = dynamic(() => import('./FieldEditModal'));
const RenderList: {
types: FlowNodeOutputTypeEnum[];
@@ -32,25 +35,40 @@ const RenderOutput = ({
const outputString = useMemo(() => JSON.stringify(flowOutputList), [flowOutputList]);
const copyOutputs = useMemo(() => {
const parseOutputs = JSON.parse(outputString) as FlowNodeOutputItemType[];
return parseOutputs;
return JSON.parse(outputString) as FlowNodeOutputItemType[];
}, [outputString]);
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<FlowNodeOutputItemType>();
const RenderDynamicOutputs = useMemo(() => {
const dynamicOutputs = copyOutputs.filter(
(item) => item.type === FlowNodeOutputTypeEnum.dynamic
);
const addOutput = dynamicOutputs.find((item) => item.key === NodeOutputKeyEnum.addOutputParam);
const filterAddOutput = dynamicOutputs.filter(
(item) => item.key !== NodeOutputKeyEnum.addOutputParam
);
return dynamicOutputs.length === 0 || !addOutput ? null : (
const onSubmit = ({ data }: { data: FlowNodeOutputItemType }) => {
if (!editField) return;
if (editField.key) {
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editField.key,
value: data
});
} else {
onChangeNode({
nodeId,
type: 'addOutput',
value: data
});
}
};
return !addOutput?.customFieldConfig ? null : (
<Box mb={5}>
<Flex
mb={2}
@@ -62,7 +80,7 @@ const RenderOutput = ({
<Box position={'relative'} fontWeight={'medium'}>
{t('core.workflow.Custom outputs')}
</Box>
<QuestionTip ml={1} label={addOutput.description} />
{addOutput.description && <QuestionTip ml={1} label={addOutput.description} />}
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
@@ -70,86 +88,22 @@ const RenderOutput = ({
iconSpacing={1}
size={'sm'}
onClick={() => {
setCreateField({});
setEditField(defaultOutput);
}}
>
{t('common.Add New')}
</Button>
</Flex>
<VariableTable
fieldEditType={addOutput.editField}
keys={copyOutputs.map((output) => output.key)}
onCloseFieldEdit={() => {
setCreateField(undefined);
setEditField(undefined);
}}
variables={filterAddOutput.map((output) => ({
label: output.label || '-',
type: output.valueType ? t(FlowValueTypeMap[output.valueType]?.label) : '-',
key: output.key
}))}
createField={createField}
onCreate={({ data }) => {
if (!data.key) {
return;
}
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
type: FlowNodeOutputTypeEnum.dynamic,
key: data.key,
valueType: data.valueType,
label: data.key
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
setCreateField(undefined);
}}
editField={editField}
onStartEdit={(e) => {
const output = copyOutputs.find((output) => output.key === e);
onEdit={(key) => {
const output = copyOutputs.find((output) => output.key === key);
if (!output) return;
setEditField({
valueType: output.valueType,
required: output.required,
key: output.key,
label: output.label,
description: output.description
});
}}
onEdit={({ data, changeKey }) => {
if (!data.key || !editField?.key) return;
const output = copyOutputs.find((output) => output.key === editField.key);
const newOutput: FlowNodeOutputItemType = {
...(output as FlowNodeOutputItemType),
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
nodeId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
setEditField(output);
}}
onDelete={(key) => {
onChangeNode({
@@ -159,9 +113,19 @@ const RenderOutput = ({
});
}}
/>
{!!editField && (
<FieldEditModal
customFieldConfig={addOutput?.customFieldConfig}
defaultValue={editField}
keys={copyOutputs.map((output) => output.key)}
onClose={() => setEditField(undefined)}
onSubmit={onSubmit}
/>
)}
</Box>
);
}, [copyOutputs, createField, editField, nodeId, onChangeNode, t]);
}, [copyOutputs, editField, nodeId, onChangeNode, t]);
const RenderCommonOutputs = useMemo(() => {
const renderOutputs = copyOutputs.filter(
@@ -172,14 +136,14 @@ const RenderOutput = ({
<>
{renderOutputs.map((output) => {
return output.label ? (
<Box key={output.key} _notLast={{ mb: 5 }} position={'relative'}>
{output.required && (
<Box position={'absolute'} left={'-6px'} top={-1} color={'red.600'}>
*
</Box>
)}
<FormLabel
key={output.key}
required={output.required}
_notLast={{ mb: 5 }}
position={'relative'}
>
<OutputLabel nodeId={nodeId} output={output} />
</Box>
</FormLabel>
) : null;
})}
</>

View File

@@ -13,7 +13,6 @@ import {
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { defaultEditFormData } from './constants';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -21,6 +20,8 @@ import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { fnValueTypeSelect } from '@/web/core/workflow/constants/dataType';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
const EditFieldModal = ({
defaultValue = defaultEditFormData,
@@ -38,8 +39,11 @@ const EditFieldModal = ({
const { mutate: onclickSubmit } = useRequest({
mutationFn: async (e: FlowNodeInputItemType) => {
e.key = e.key.trim();
const inputConfig: FlowNodeInputItemType = {
...e,
description: e.toolDescription,
label: e.key
};
if (defaultValue.key) {
@@ -136,3 +140,17 @@ const EditFieldModal = ({
};
export default React.memo(EditFieldModal);
export const defaultEditFormData: FlowNodeInputItemType = {
valueType: WorkflowIOValueTypeEnum.string,
renderTypeList: [FlowNodeInputTypeEnum.reference],
key: '',
label: '',
toolDescription: '',
required: true,
canEdit: true,
customInputConfig: {
selectValueTypeList: Object.values(fnValueTypeSelect).map((item) => item.value),
showDescription: true
}
};

View File

@@ -1,21 +0,0 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
export const defaultEditFormData: FlowNodeInputItemType = {
valueType: WorkflowIOValueTypeEnum.string,
renderTypeList: [FlowNodeInputTypeEnum.reference],
key: '',
label: '',
toolDescription: '',
required: true,
canEdit: true,
editField: {
key: true,
description: true
}
};
export default function Dom() {
return <></>;
}

View File

@@ -1,8 +1,5 @@
import React, { useMemo, useState } from 'react';
import type {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import {
Box,
Button,
@@ -13,52 +10,56 @@ import {
Th,
Td,
TableContainer,
Flex
Flex,
HStack
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { defaultEditFormData } from './constants';
import { defaultEditFormData } from './EditFieldModal';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import IOTitle from '../../../components/IOTitle';
import { SmallAddIcon } from '@chakra-ui/icons';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
const EditFieldModal = dynamic(() => import('./EditFieldModal'));
const RenderToolInput = ({
nodeId,
inputs,
canEdit = false
inputs
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
canEdit?: boolean;
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { toolInputs } = splitToolInputs(inputs, nodeId);
const dynamicInput = useMemo(() => {
return inputs.find((item) => item.renderTypeList[0] === FlowNodeInputTypeEnum.addInputParam);
}, [inputs]);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
return (
<>
{canEdit && (
<Flex mb={2} alignItems={'center'}>
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
{t('common.Field')}
</Box>
<HStack mb={2} justifyContent={'space-between'}>
<IOTitle text={t('core.module.tool.Tool input')} mb={0} />
{dynamicInput && (
<Button
variant={'unstyled'}
leftIcon={<MyIcon name={'common/addLight'} w={'14px'} />}
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
px={3}
_hover={{
bg: 'myGray.150'
}}
onClick={() => setEditField(defaultEditFormData)}
>
{t('core.module.extract.Add field')}
{t('Add new')}
</Button>
</Flex>
)}
<Box borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom="none">
)}
</HStack>
<Box borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
@@ -66,11 +67,11 @@ const RenderToolInput = ({
<Th></Th>
<Th></Th>
<Th></Th>
{canEdit && <Th></Th>}
{dynamicInput && <Th></Th>}
</Tr>
</Thead>
<Tbody>
{inputs.map((item, index) => (
{toolInputs.map((item, index) => (
<Tr
key={index}
position={'relative'}
@@ -80,7 +81,7 @@ const RenderToolInput = ({
<Td>{item.key}</Td>
<Td>{item.toolDescription}</Td>
<Td>{item.required ? '✔' : ''}</Td>
{canEdit && (
{dynamicInput && (
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}

View File

@@ -1,4 +1,4 @@
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import { Box } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';

View File

@@ -1,123 +0,0 @@
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import type {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('./FieldEditModal'));
const VariableTable = ({
fieldEditType,
variables = [],
keys,
createField,
onCreate,
editField,
onStartEdit,
onEdit,
onCloseFieldEdit,
onDelete
}: {
fieldEditType?: EditInputFieldMapType;
variables: { icon?: string; label: string; type: string; key: string }[];
keys: string[];
createField?: EditNodeFieldType;
onCreate?: (e: { data: EditNodeFieldType }) => void;
editField?: EditNodeFieldType;
onStartEdit: (key: string) => void;
onEdit?: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
onCloseFieldEdit: () => void;
onDelete?: (key: string) => void;
}) => {
const { t } = useTranslation();
const fileEditData = (createField || editField) as EditNodeFieldType | undefined;
return (
<>
<Box
bg={'white'}
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom={'none'}
>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('core.module.variable.variable name')}
</Th>
<Th>{t('core.workflow.Value type')}</Th>
<Th borderBottomRightRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item) => (
<Tr key={item.key}>
<Td>
<Flex alignItems={'center'}>
{!!item.icon && <MyIcon name={item.icon as any} w={'14px'} mr={1} />}
{item.label || item.key}
</Flex>
</Td>
<Td>{item.type}</Td>
<Td>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => onStartEdit(item.key)}
/>
<MyIcon
className="delete"
name={'delete'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onDelete?.(item.key);
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
{!!fileEditData && (
<FieldEditModal
editField={fieldEditType}
defaultField={fileEditData}
keys={keys}
onClose={onCloseFieldEdit}
onSubmit={(e) => {
if (!!createField && onCreate) {
onCreate(e);
} else if (!!editField && onEdit) {
onEdit(e);
}
}}
/>
)}
</>
);
};
export default React.memo(VariableTable);

Some files were not shown because too many files have changed in this diff Show More