Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c4de40e
feat(workflow): invalid variable check
Nixieboluo Aug 26, 2026
dd4a5aa
feat(workflow): invalid reference type check
Nixieboluo Aug 26, 2026
b66e96d
feat(workflow): unreachable reference check
Nixieboluo Aug 27, 2026
946f40b
feat(workflow): preserve reference snapshots
Nixieboluo Aug 27, 2026
52d063e
feat(workflow): unify variable references
Nixieboluo Aug 31, 2026
85794ae
fix(workflow): unify reference checks
Nixieboluo Aug 31, 2026
9e35954
Merge branch 'main' into feat/global-var-check
Nixieboluo Sep 1, 2026
d50fe22
test: update tests
Nixieboluo Sep 1, 2026
c17c8f6
fix(workflow): handle invalid references safely
Nixieboluo Sep 1, 2026
585a443
refactor(workflow): simplify reference handling
Nixieboluo Sep 1, 2026
60c2117
feat: invalid label style (temp)
Nixieboluo Sep 1, 2026
8e81ddf
refactor(workflow): no reference snapshot migrations
Nixieboluo Sep 1, 2026
20f19b6
refactor(workflow): capture deleted reference labels
Nixieboluo Sep 1, 2026
bf30f2e
fix(workflow): pass reference metadata
Nixieboluo Sep 2, 2026
104f8c9
fix(workflow): preserve reference labels
Nixieboluo Sep 2, 2026
ebb83be
fix(workflow): check affected references
Nixieboluo Sep 2, 2026
9a9a176
fix(workflow): handles self references check in http node
Nixieboluo Sep 2, 2026
0b90fc6
fix(workflow): align reference checks
Nixieboluo Sep 2, 2026
8c9cc99
chore: merge main
Nixieboluo Sep 2, 2026
a98cd2c
fix(workflow): preserve tool references
Nixieboluo Sep 3, 2026
425fc86
Merge branch 'main' into feat/global-var-check
Nixieboluo Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions document/content/guide/admin/sso.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -501,10 +501,10 @@ LDAP_MEMBERSHIP_MODE supports four modes:

| Mode | Relationship | Main configuration |
| ---------------- | --------------------------------------------------------------- | ------------------------ |
| user_attribute | A user attribute, usually ou, points to an organization | LDAP_USER_ATTRIBUTE_* |
| group_member_dn | A group attribute, usually member, contains user DNs | LDAP_GROUP_MEMBER_DN_* |
| user_member_of | A user attribute, usually memberOf, contains group DNs | LDAP_USER_MEMBER_OF_* |
| posix_member_uid | A group attribute, usually memberUid, contains user identifiers | LDAP_POSIX_MEMBER_UID_* |
| user_attribute | A user attribute, usually ou, points to an organization | LDAP*USER_ATTRIBUTE*\* |
| group_member_dn | A group attribute, usually member, contains user DNs | LDAP*GROUP_MEMBER_DN*\* |
| user_member_of | A user attribute, usually memberOf, contains group DNs | LDAP*USER_MEMBER_OF*\* |
| posix_member_uid | A group attribute, usually memberUid, contains user identifiers | LDAP*POSIX_MEMBER_UID*\* |

**1. user_attribute: user attribute points to an organization**

Expand Down
6 changes: 3 additions & 3 deletions document/data/doc-last-modified.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"content/faq/chat.mdx": "2026-04-26T21:08:47+08:00",
"content/faq/index.en.mdx": "2026-06-04T16:10:15+08:00",
"content/faq/index.mdx": "2026-06-04T16:10:15+08:00",
"content/guide/admin/sso.en.mdx": "2026-09-02T14:13:44+08:00",
"content/guide/admin/sso.en.mdx": "2026-09-02T18:52:57+08:00",
"content/guide/admin/sso.mdx": "2026-09-02T14:13:44+08:00",
"content/guide/admin/teamMode.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/admin/teamMode.mdx": "2026-05-07T15:06:40+08:00",
Expand Down Expand Up @@ -355,8 +355,8 @@
"content/self-host/upgrading/4-16/4161.mdx": "2026-08-24T10:06:28+08:00",
"content/self-host/upgrading/4-16/4162.en.mdx": "2026-09-01T22:54:35+08:00",
"content/self-host/upgrading/4-16/4162.mdx": "2026-09-01T22:54:35+08:00",
"content/self-host/upgrading/4-16/4163.en.mdx": "2026-09-02T18:31:54+08:00",
"content/self-host/upgrading/4-16/4163.mdx": "2026-09-02T18:31:54+08:00",
"content/self-host/upgrading/4-16/4163.en.mdx": "2026-09-02T17:26:55+08:00",
"content/self-host/upgrading/4-16/4163.mdx": "2026-09-02T17:26:55+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-08-16T23:16:43+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-08-16T23:16:43+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-08-16T23:16:43+08:00",
Expand Down
11 changes: 11 additions & 0 deletions packages/global/core/app/formEdit/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ export const canInputBeAgentGenerated = (
return !input.renderTypeList.some((type) => agentGeneratedDenyRenderTypes.has(type));
};

/** 判断输入是否为工作流工具参数。 */
export const isToolParamInput = (
input: Pick<
FlowNodeInputItemType,
'key' | 'canEdit' | 'defaultToAgentGenerated' | 'renderTypeList' | 'canAgentGenerated'
>
) =>
input.canEdit === true &&
input.defaultToAgentGenerated === true &&
canInputBeAgentGenerated(input);

/**
* 归一当前节点输入的可选来源和默认选择。
*
Expand Down
107 changes: 106 additions & 1 deletion packages/global/core/workflow/migration/legacy/workflow.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充注释

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,111 @@ import {
} from '../../utils';
import type { NodeToolConfigType } from '../../type/node';

const isReferenceItem = (value: unknown): value is [string, string] =>
Array.isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string' &&
typeof value[1] === 'string' &&
value[0].length > 0 &&
value[1].length > 0;

const isEmptyReferenceValue = (value: unknown) =>
value === undefined ||
value === null ||
value === '' ||
(Array.isArray(value) &&
(value.length === 0 ||
(value.length === 2 &&
((value[0] === '' && value[1] === '') ||
(value[0] === undefined && value[1] === undefined)))));

const cleanReferenceValue = (value: unknown, isArray: boolean) => {
if (isArray) {
if (isReferenceItem(value)) return [value];
if (!Array.isArray(value)) return isEmptyReferenceValue(value) ? value : [];
return value.filter(isReferenceItem);
}

if (isReferenceItem(value)) return value;
if (Array.isArray(value)) {
const referenceItems = value.filter(isReferenceItem);
if (referenceItems.length === 1) return referenceItems[0];
}
return isEmptyReferenceValue(value) ? value : undefined;
};

const cleanReferenceFields = (node: CanonicalWorkflowData['nodes'][number]) => {
const cleanInput = (input: CanonicalWorkflowData['nodes'][number]['inputs'][number]) => {
const nextInput = { ...input };
if (nodeInputIsReference(input)) {
const value = cleanReferenceValue(input.value, input.valueType?.startsWith('array') === true);
if (value === undefined) delete nextInput.value;
else nextInput.value = value;
}

if (
node.flowNodeType === FlowNodeTypeEnum.ifElseNode &&
input.key === NodeInputKeyEnum.ifElseList
) {
nextInput.value = Array.isArray(input.value)
? input.value.map((branch) => {
if (!branch || typeof branch !== 'object' || Array.isArray(branch)) return branch;
const nextBranch = { ...(branch as Record<string, unknown>) };
nextBranch.list = Array.isArray(nextBranch.list)
? nextBranch.list.map((condition) => {
if (!condition || typeof condition !== 'object' || Array.isArray(condition)) {
return condition;
}
const nextCondition = { ...(condition as Record<string, unknown>) };
const variable = cleanReferenceValue(nextCondition.variable, false);
if (variable === undefined) delete nextCondition.variable;
else nextCondition.variable = variable;
if (nextCondition.valueType === 'reference') {
const value = cleanReferenceValue(nextCondition.value, false);
if (value === undefined) delete nextCondition.value;
else nextCondition.value = value;
}
return nextCondition;
})
: nextBranch.list;
return nextBranch;
})
: input.value;
}

if (
node.flowNodeType === FlowNodeTypeEnum.variableUpdate &&
input.key === NodeInputKeyEnum.updateList
) {
nextInput.value = Array.isArray(input.value)
? input.value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
const nextItem = { ...(item as Record<string, unknown>) };
const variable = cleanReferenceValue(nextItem.variable, false);
if (variable === undefined) delete nextItem.variable;
else nextItem.variable = variable;
if (nextItem.renderType === FlowNodeInputTypeEnum.reference) {
const value = cleanReferenceValue(
nextItem.value,
typeof nextItem.valueType === 'string' && nextItem.valueType.startsWith('array')
);
if (value === undefined) delete nextItem.value;
else nextItem.value = value;
}
return nextItem;
})
: input.value;
}

return nextInput;
};

return {
...node,
inputs: node.inputs.map(cleanInput)
};
};

// 这些旧插件没有持久化 catchError,但当前运行时需要显式开启错误分支。
const legacyCatchErrorPluginIds = new Set([
'systemTool-dalle3',
Expand Down Expand Up @@ -193,7 +298,7 @@ const migrateLegacyCanvasStructure = (workflow: CanonicalWorkflowData): Canonica
});
});

return { ...workflow, nodes, edges };
return { ...workflow, nodes: nodes.map(cleanReferenceFields), edges };
};
/**
* 将无需资源解析的历史结构收敛为当前 workflow 结构。
Expand Down
2 changes: 1 addition & 1 deletion packages/global/core/workflow/migration/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { NodeToolConfigTypeSchema, StoreNodeItemTypeSchema } from '../type/node'
import { StoreEdgeItemTypeSchema } from '../type/edge';

/**
* 当前工作流输入。该类型不包含任何历史字段
* 当前工作流输入。引用快照是可选的历史展示字段,不参与运行时引用解析
*/
export const CanonicalFlowNodeInputItemSchema = FlowNodeInputItemTypeSchema;
export type CanonicalFlowNodeInputItem = z.infer<typeof CanonicalFlowNodeInputItemSchema>;
Expand Down
12 changes: 11 additions & 1 deletion packages/global/core/workflow/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { isValidReferenceValueFormat } from '../utils';
import type { RuntimeNodeItemType } from './type';
import { isSecretValue } from '../../../common/secret/utils';
import { isChildInteractive } from '../template/system/interactive/constants';
import { isToolParamInput } from '../../app/formEdit/utils';

export const extractDeepestInteractive = (
interactive: WorkflowInteractiveResponseType
Expand Down Expand Up @@ -333,7 +334,16 @@ export const getReferenceVariableValue = ({
return value;
}

return node.outputs.find((output) => output.id === outputId)?.value;
const output = node.outputs.find((item) => item.id === outputId);
if (output) return output.value;

// HTTP 工具参数是编辑器侧的虚拟输出,运行时值仍保存在节点输入中。
if (node.flowNodeType === FlowNodeTypeEnum.httpRequest468) {
const input = node.inputs.find((item) => item.key === outputId && isToolParamInput(item));
if (input) return input.value ?? input.defaultValue;
}

return undefined;
};

// handle single reference value
Expand Down
4 changes: 3 additions & 1 deletion packages/global/core/workflow/template/system/ifElse/type.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { ReferenceItemValueType } from '../../../type/io';
import type { ReferenceItemValueType, WorkflowReferenceSnapshot } from '../../../type/io';
import type { VariableConditionEnum } from './constant';

export type IfElseConditionType = 'AND' | 'OR';
export type ConditionListItemType = {
variable?: ReferenceItemValueType;
variableSnapshot?: WorkflowReferenceSnapshot;
condition?: VariableConditionEnum;
value?: string | ReferenceItemValueType;
valueSnapshot?: WorkflowReferenceSnapshot;
valueType?: 'input' | 'reference';
};
export type IfElseListItemType = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import type { FlowNodeInputTypeEnum } from '../../../node/constant';
import type { ReferenceItemValueType, ReferenceValueType } from '../../..//type/io';
import type {
ReferenceItemValueType,
ReferenceValueType,
WorkflowReferenceSnapshot
} from '../../..//type/io';
import type { WorkflowIOValueTypeEnum } from '../../../constants';

export type TUpdateListItem = {
variable?: ReferenceItemValueType;
variableSnapshot?: WorkflowReferenceSnapshot;
value?: ReferenceValueType; // input: ['',value], reference: [nodeId,outputId]
valueReferenceSnapshots?: WorkflowReferenceSnapshot[];
valueType?: WorkflowIOValueTypeEnum;
renderType: FlowNodeInputTypeEnum.input | FlowNodeInputTypeEnum.reference;

Expand Down
42 changes: 31 additions & 11 deletions packages/global/core/workflow/type/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ export const CustomFieldConfigTypeSchema = z.object({
});
export type CustomFieldConfigType = z.infer<typeof CustomFieldConfigTypeSchema>;

/* Reference */
export const ReferenceItemValueTypeSchema = z.tuple([z.string(), z.string()]);
export type ReferenceItemValueType = z.infer<typeof ReferenceItemValueTypeSchema>;
export const ReferenceArrayValueTypeSchema = z.array(ReferenceItemValueTypeSchema);
export type ReferenceArrayValueType = z.infer<typeof ReferenceArrayValueTypeSchema>;
export const ReferenceValueTypeSchema = z.union([
ReferenceItemValueTypeSchema,
ReferenceArrayValueTypeSchema
]);
export type ReferenceValueType = z.infer<typeof ReferenceValueTypeSchema>;

/**
* 引用来源失效后的历史展示信息。reference 用于多选和嵌套配置按稳定身份匹配快照。
* 状态和类型均由当前工作流实时计算,不在快照中持久化。
*/
export const WorkflowReferenceSnapshotSchema = z.object({
reference: ReferenceItemValueTypeSchema,
sourceLabel: z.string().optional(),
outputLabel: z.string().optional(),
// 来源节点图标;变量自身图标不进入 snapshot。
icon: z.string().optional()
});
export type WorkflowReferenceSnapshot = z.infer<typeof WorkflowReferenceSnapshotSchema>;

export const InputComponentPropsTypeSchema = z.object({
key: z.enum(NodeInputKeyEnum).or(z.string()).meta({
description: '变量键名,用于在提示词或工作流中引用该输入'
Expand Down Expand Up @@ -271,6 +295,10 @@ export const FlowNodeInputItemTypeSchema = InputComponentPropsTypeSchema.extend(
description: '节点输入当前值'
}),

referenceSnapshots: z.array(WorkflowReferenceSnapshotSchema).optional().meta({
description: '节点输入引用的历史展示快照'
}),

debugLabel: z.string().optional().meta({
description: '调试模式下展示的输入名称'
}),
Expand Down Expand Up @@ -329,6 +357,9 @@ export const FlowNodeOutputItemTypeSchema = z.object({
valueType: z.enum(WorkflowIOValueTypeEnum).optional().meta({
description: '节点输出值的数据类型'
}),
icon: z.string().optional().meta({
description: '节点输出变量图标'
}),
valueDesc: z.string().optional().meta({
description: '输出值说明,通常用于展示引用值含义'
}),
Expand Down Expand Up @@ -380,17 +411,6 @@ export const FlowNodeOutputItemTypeSchema = z.object({
});
export type FlowNodeOutputItemType = z.infer<typeof FlowNodeOutputItemTypeSchema>;

/* Reference */
export const ReferenceItemValueTypeSchema = z.tuple([z.string(), z.string().optional()]);
export type ReferenceItemValueType = z.infer<typeof ReferenceItemValueTypeSchema>;
export const ReferenceArrayValueTypeSchema = z.array(ReferenceItemValueTypeSchema);
export type ReferenceArrayValueType = z.infer<typeof ReferenceArrayValueTypeSchema>;
export const ReferenceValueTypeSchema = z.union([
ReferenceItemValueTypeSchema,
ReferenceArrayValueTypeSchema
]);
export type ReferenceValueType = z.infer<typeof ReferenceValueTypeSchema>;

/* http node */
export const HttpParamAndHeaderItemTypeSchema = z.object({
key: z.string(),
Expand Down
2 changes: 2 additions & 0 deletions packages/global/core/workflow/type/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ export type FlowNodeItemType = z.infer<typeof FlowNodeItemSchema>;
// store node type
export const StoreNodeItemTypeSchema = FlowNodeCommonTypeSchema.extend({
nodeId: z.string(),
/** 画布节点折叠状态,需要随工作流版本持久化。 */
isFolded: BoolSchema.optional(),
position: z
.object({
x: NumSchema,
Expand Down
11 changes: 7 additions & 4 deletions packages/global/core/workflow/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,13 @@ export const toolSetData2FlowNodeIO = ({ nodes }: { nodes: StoreNodeItemType[] }
export const formatEditorVariablePickerIcon = (
variables: { key: string; label: string; type?: `${VariableInputEnum}`; required?: boolean }[]
): EditorVariablePickerType[] => {
return variables.map((item) => ({
...item,
icon: item.type ? variableMap[item.type]?.icon : variableMap['input'].icon
}));
return variables.map((item) => {
const icon = item.type ? variableMap[item.type]?.icon : undefined;
return {
...item,
...(icon ? { icon } : {})
};
});
};

// Check the value is a valid reference value format: [variableId, outputId]
Expand Down
Loading
Loading