Commit 88ed97bc by Archer Committed by GitHub

fix: workflow batch repeat run (#6186)

* stop design doc

* remove invalid doc

* perf: auto fit

* fix: icon

* perf: icon

* perf: icon

* perf: icon

* perf: icon

* perf: variable disabled input ui

* fix: workflow batch run

* fix: tsc
parent 1e68a5fe
......@@ -17,14 +17,16 @@ description: 'FastGPT V4.14.5 更新说明'
1. 优化获取 redis 所有 key 的逻辑,避免大量获取时导致阻塞。
2. MongoDB, Redis 和 MQ 的重连逻辑优化。
3. 变量输入框禁用状态可复制。
## 🐛 修复
1. MCP 工具创建时,使用自定义鉴权头会报错。
2. 获取对话日志列表时,如果用户头像为空,会抛错。
3. chatAgent 未开启问题优化时,前端 UI 显示开启。
4. 加载默认模型时,maxTokens 字段未赋值,导致模型最大响应值配置为空。
5. S3 文件清理队列因网络稳定问题出现阻塞,导致删除任务不再执行。
1. 重要 - 工作流并行合并后,可能导致重复运行问题。
2. MCP 工具创建时,使用自定义鉴权头会报错。
3. 获取对话日志列表时,如果用户头像为空,会抛错。
4. chatAgent 未开启问题优化时,前端 UI 显示开启。
5. 加载默认模型时,maxTokens 字段未赋值,导致模型最大响应值配置为空。
6. S3 文件清理队列因网络稳定问题出现阻塞,导致删除任务不再执行。
## 插件
......@@ -120,7 +120,7 @@
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00",
"document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00",
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00",
"document/content/docs/upgrading/4-14/4145.mdx": "2025-12-30T11:31:47+08:00",
"document/content/docs/upgrading/4-14/4145.mdx": "2026-01-05T11:19:08+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
......
......@@ -387,7 +387,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
this.processActiveNode();
}
// Process next active node
private processActiveNode() {
private async processActiveNode() {
// Finish
if (this.activeRunQueue.size === 0 && this.runningNodeCount === 0) {
if (isDebugMode) {
......@@ -414,6 +414,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
return;
}
// Thread avoidance
await surrenderProcess();
const nodeId = this.activeRunQueue.keys().next().value;
const node = nodeId ? this.runtimeNodesMap.get(nodeId) : undefined;
......@@ -443,7 +445,9 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
this.skipNodeQueue.set(node.nodeId, { node, skippedNodeIdList: concatSkippedNodeIdList });
}
private processSkipNodes() {
private async processSkipNodes() {
// Thread avoidance
await surrenderProcess();
// 取一个 node,并且从队列里删除
const skipItem = this.skipNodeQueue.values().next().value;
if (skipItem) {
......@@ -867,9 +871,6 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
return;
}
// Thread avoidance
await surrenderProcess();
addLog.debug(`Run node`, { maxRunTimes: data.maxRunTimes, appId: data.runningAppInfo.id });
// Get node run status by edges
......@@ -881,6 +882,13 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
const nodeRunResult = await (async () => {
if (status === 'run') {
// All source edges status to waiting
runtimeEdges.forEach((item) => {
if (item.target === node.nodeId) {
item.status = 'waiting';
}
});
const blanceCheckResult = await this.checkTeamBlance();
if (blanceCheckResult) {
return {
......@@ -890,13 +898,6 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
};
}
// All source edges status to waiting
runtimeEdges.forEach((item) => {
if (item.target === node.nodeId) {
item.status = 'waiting';
}
});
addLog.debug(`[dispatchWorkFlow] nodeRunWithActive: ${node.name}`);
return this.nodeRunWithActive(node);
}
......
......@@ -43,6 +43,7 @@ import MarkdownPlugin from './plugins/MarkdownPlugin';
import MyIcon from '../../Icon';
import ListExitPlugin from './plugins/ListExitPlugin';
import KeyDownPlugin from './plugins/KeyDownPlugin';
import EditablePlugin from './plugins/EditablePlugin';
const Placeholder = ({ children, padding }: { children: React.ReactNode; padding: string }) => (
<Box
......@@ -80,6 +81,7 @@ export type EditorProps = {
placeholder?: string;
placeholderPadding?: string;
isInvalid?: boolean;
isDisabled?: boolean;
onKeyDown?: (e: React.KeyboardEvent) => void;
ExtensionPopover?: ((e: {
onChangeText: (text: string) => void;
......@@ -105,6 +107,7 @@ export default function Editor({
placeholderPadding = '12px 14px',
bg = 'white',
isInvalid,
isDisabled = false,
onKeyDown,
ExtensionPopover,
boxStyle
......@@ -179,7 +182,13 @@ export default function Editor({
<RichTextPlugin
contentEditable={
<ContentEditable
className={`${isInvalid ? styles.contentEditable_invalid : styles.contentEditable} ${styles.richText}`}
className={`${
isDisabled
? styles.contentEditable_disabled
: isInvalid
? styles.contentEditable_invalid
: styles.contentEditable
} ${styles.richText}`}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`,
......@@ -194,7 +203,13 @@ export default function Editor({
<PlainTextPlugin
contentEditable={
<ContentEditable
className={isInvalid ? styles.contentEditable_invalid : styles.contentEditable}
className={
isDisabled
? styles.contentEditable_disabled
: isInvalid
? styles.contentEditable_invalid
: styles.contentEditable
}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`,
......@@ -211,8 +226,9 @@ export default function Editor({
<>
<HistoryPlugin />
<MaxLengthPlugin maxLength={maxLength || 999999} />
<FocusPlugin focus={focus} setFocus={setFocus} />
<FocusPlugin focus={focus} setFocus={setFocus} isDisabled={isDisabled} />
<KeyDownPlugin onKeyDown={onKeyDown} />
<EditablePlugin isDisabled={isDisabled || !onChangeText} />
{variableLabels.length > 0 && (
<>
......
......@@ -72,6 +72,29 @@
box-shadow: 0px 0px 0px 2.4px rgba(244, 69, 46, 0.15);
}
.contentEditable_disabled {
position: relative;
height: 100%;
width: 100%;
border: 1px solid rgb(232, 235, 240);
border-radius: var(--chakra-radii-sm);
padding: 8px 12px;
font-size: var(--chakra-fontSizes-sm);
overflow-y: auto;
&::-webkit-scrollbar {
color: var(--chakra-colors-myGray-100);
}
&::-webkit-scrollbar-thumb {
background-color: var(--chakra-colors-myGray-200) !important;
cursor: pointer;
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--chakra-colors-myGray-250) !important;
}
}
.variable {
color: var(--chakra-colors-primary-600);
padding: 0 2px;
......
......@@ -68,6 +68,7 @@ const PromptEditor = ({
onChangeText={onChange}
onBlur={onBlurInput}
onKeyDown={onKeyDown}
isDisabled={isDisabled}
/>
{isDisabled && (
<Box
......@@ -76,10 +77,11 @@ const PromptEditor = ({
left={0}
right={0}
bottom={0}
bg="rgba(255, 255, 255, 0.4)"
bg="rgba(255, 255, 255, 0.5)"
borderRadius="md"
zIndex={1}
cursor="not-allowed"
pointerEvents="none"
/>
)}
</Box>
......@@ -92,17 +94,34 @@ const PromptEditor = ({
w={'full'}
>
<ModalBody>
<Editor
{...props}
minH={400}
maxH={400}
showOpenModal={false}
value={formattedValue}
onChange={onChangeInput}
onChangeText={onChange}
onBlur={onBlurInput}
onKeyDown={onKeyDown}
/>
<Box position="relative">
<Editor
{...props}
minH={400}
maxH={400}
showOpenModal={false}
value={formattedValue}
onChange={onChangeInput}
onChangeText={onChange}
onBlur={onBlurInput}
onKeyDown={onKeyDown}
isDisabled={isDisabled}
/>
{isDisabled && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
bg="rgba(255, 255, 255, 0.5)"
borderRadius="md"
zIndex={1}
cursor="not-allowed"
pointerEvents="none"
/>
)}
</Box>
</ModalBody>
<ModalFooter>
<Button mr={2} onClick={onClose} px={6}>
......
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
export default function EditablePlugin({ isDisabled }: { isDisabled: boolean }) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
editor.setEditable(!isDisabled);
}, [editor, isDisabled]);
return null;
}
......@@ -2,7 +2,15 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { useEffect } from 'react';
import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical';
export default function FocusPlugin({ focus, setFocus }: { focus: Boolean; setFocus: any }) {
export default function FocusPlugin({
focus,
setFocus,
isDisabled
}: {
focus: Boolean;
setFocus: any;
isDisabled?: boolean;
}) {
const [editor] = useLexicalComposerContext();
useEffect(
......@@ -10,12 +18,14 @@ export default function FocusPlugin({ focus, setFocus }: { focus: Boolean; setFo
editor.registerCommand(
BLUR_COMMAND,
() => {
setFocus(false);
if (!isDisabled) {
setFocus(false);
}
return false;
},
COMMAND_PRIORITY_LOW
),
[]
[isDisabled]
);
useEffect(
......@@ -23,12 +33,14 @@ export default function FocusPlugin({ focus, setFocus }: { focus: Boolean; setFo
editor.registerCommand(
FOCUS_COMMAND,
() => {
setFocus(true);
if (!isDisabled) {
setFocus(true);
}
return false;
},
COMMAND_PRIORITY_LOW
),
[]
[isDisabled]
);
// useEffect(() => {
......
......@@ -35,6 +35,7 @@ const LabelAndFormRender = ({
placeholder,
inputType,
showValueType,
isUnChange,
...props
}: {
label: string | React.ReactNode;
......@@ -45,6 +46,7 @@ const LabelAndFormRender = ({
fieldName: string;
isDisabled?: boolean;
isUnChange?: boolean;
minLength?: number;
} & SpecificProps &
BoxProps) => {
......@@ -98,7 +100,7 @@ const LabelAndFormRender = ({
inputType={inputType}
isRichText={false}
value={value}
onChange={onChange}
onChange={isUnChange ? undefined : onChange}
placeholder={placeholder}
isInvalid={!!error}
{...props}
......
......@@ -101,7 +101,7 @@ const InputRender = (props: InputRenderProps) => {
{...commonProps}
onChange={(e) => {
const val = e.target.value;
onChange({
onChange?.({
value: val,
secret: ''
});
......@@ -165,7 +165,7 @@ const InputRender = (props: InputRenderProps) => {
return (
<Switch
isChecked={value}
onChange={(e) => onChange(e.target.checked)}
onChange={(e) => onChange?.(e.target.checked)}
isDisabled={isDisabled}
/>
);
......@@ -186,7 +186,7 @@ const InputRender = (props: InputRenderProps) => {
h={10}
list={list}
value={value}
onSelect={onChange}
onSelect={(e) => onChange?.(e)}
isSelectAll={isSelectAll}
itemWrap
/>
......@@ -217,7 +217,7 @@ const InputRender = (props: InputRenderProps) => {
return (
<FileSelector
value={files}
onChange={onChange}
onChange={(e) => onChange?.(e)}
isDisabled={isDisabled}
maxFiles={props.maxFiles}
canSelectFile={props.canSelectFile}
......@@ -251,7 +251,7 @@ const InputRender = (props: InputRenderProps) => {
list={list ?? []}
value={selectedValues}
onSelect={(selectedVals) => {
onChange(
onChange?.(
selectedVals.map((val) => {
const selectedItem = list?.find((item) => item.value === val);
return {
......@@ -274,7 +274,7 @@ const InputRender = (props: InputRenderProps) => {
return (
<TimeInput
value={val ? new Date(val) : undefined}
onDateTimeChange={(date) => onChange(date ? date.toISOString() : undefined)}
onDateTimeChange={(date) => onChange?.(date ? date.toISOString() : undefined)}
timeGranularity={props.timeGranularity}
minDate={timeRangeStart ? new Date(timeRangeStart) : undefined}
maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined}
......@@ -297,7 +297,7 @@ const InputRender = (props: InputRenderProps) => {
onDateTimeChange={(date) => {
const newArray = [...rangeArray];
newArray[0] = date ? date.toISOString() : undefined;
onChange(newArray);
onChange?.(newArray);
}}
timeGranularity={props.timeGranularity}
maxDate={
......@@ -315,7 +315,7 @@ const InputRender = (props: InputRenderProps) => {
onDateTimeChange={(date) => {
const newArray = [...rangeArray];
newArray[1] = date ? date.toISOString() : undefined;
onChange(newArray);
onChange?.(newArray);
}}
timeGranularity={props.timeGranularity}
minDate={
......
......@@ -13,7 +13,7 @@ import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io'
type CommonRenderProps = {
placeholder?: string;
value: any;
onChange: (value: any) => void;
onChange?: (value: any) => void;
isDisabled?: boolean;
isInvalid?: boolean;
......
import React, { useMemo, useState, useEffect } from 'react';
import React, { useMemo } from 'react';
import { type UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Flex } from '@chakra-ui/react';
......@@ -73,7 +73,7 @@ const VariableInputForm = ({
internalVariableList.length > 0 ||
externalVariableList.length > 0;
const isDisabled = chatType === ChatTypeEnum.log;
const isUnChange = chatType === ChatTypeEnum.log;
return hasVariables ? (
<Box py={3}>
......@@ -105,7 +105,7 @@ const VariableInputForm = ({
return (
<LabelAndFormRender
{...item}
isDisabled={isDisabled}
isUnChange={isUnChange}
key={item.key}
placeholder={item.description}
inputType={variableInputTypeToInputType(item.type, item.valueType)}
......@@ -148,7 +148,7 @@ const VariableInputForm = ({
return (
<LabelAndFormRender
{...item}
isDisabled={isDisabled}
isUnChange={isUnChange}
key={item.key}
placeholder={item.description}
inputType={variableInputTypeToInputType(item.type, item.valueType)}
......@@ -190,7 +190,7 @@ const VariableInputForm = ({
return (
<LabelAndFormRender
{...item}
isDisabled={isDisabled}
isUnChange={isUnChange}
key={item.key}
placeholder={item.description}
inputType={variableInputTypeToInputType(item.type)}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment