V4.9.6 feature (#4565)

* Dashboard submenu (#4545)

* add app submenu (#4452)

* add app submenu

* fix

* width & i18n

* optimize submenu code (#4515)

* optimize submenu code

* fix

* fix

* fix

* fix ts

* perf: dashboard sub menu

* doc

---------

Co-authored-by: heheer <heheer@sealos.io>

* feat: value format test

* doc

* Mcp export (#4555)

* feat: mcp server

* feat: mcp server

* feat: mcp server build

* update doc

* perf: path selector (#4556)

* perf: path selector

* fix: docker file path

* perf: add image endpoint to dataset search (#4557)

* perf: add image endpoint to dataset search

* fix: mcp_server url

* human in loop (#4558)

* Support interactive nodes for loops, and enhance the function of merging nested and loop node history messages. (#4552)

* feat: add LoopInteractive definition

* feat: Support LoopInteractive type and update related logic

* fix: Refactor loop handling logic and improve output value initialization

* feat: Add mergeSignId to dispatchLoop and dispatchRunAppNode responses

* feat: Enhance mergeChatResponseData to recursively merge plugin details and improve response handling

* refactor: Remove redundant comments in mergeChatResponseData for clarity

* perf: loop interactive

* perf: human in loop

---------

Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>

* mcp server ui

* integrate mcp (#4549)

* integrate mcp

* delete unused code

* fix ts

* bug fix

* fix

* support whole mcp tools

* add try catch

* fix

* fix

* fix ts

* fix test

* fix ts

* fix: interactive in v1 completions

* doc

* fix: router path

* fix mcp integrate (#4563)

* fix mcp integrate

* fix ui

* fix: mcp ux

* feat: mcp call title

* remove repeat loading

* fix mcp tools avatar (#4564)

* fix

* fix avatar

* fix update version

* update doc

* fix: value format

* close server and remove cache

* perf: avatar

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
This commit is contained in:
Archer
2025-04-16 22:18:51 +08:00
committed by GitHub
parent ab799e13cd
commit 952412f648
166 changed files with 6318 additions and 1263 deletions

View File

@@ -140,7 +140,9 @@ export enum FlowNodeTypeEnum {
loopStart = 'loopStart',
loopEnd = 'loopEnd',
formInput = 'formInput',
comment = 'comment'
comment = 'comment',
tool = 'tool',
toolSet = 'toolSet'
}
// node IO value type

View File

@@ -217,6 +217,8 @@ export type DispatchNodeResponseType = {
// tool params
toolParamsResult?: Record<string, any>;
toolRes?: any;
// abandon
extensionModel?: string;
extensionResult?: string;

View File

@@ -10,6 +10,7 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
import { replaceVariable, valToStr } from '../../../common/string/tools';
import json5 from 'json5';
import {
InteractiveNodeResponseType,
WorkflowInteractiveResponseType
@@ -18,7 +19,10 @@ import {
export const extractDeepestInteractive = (
interactive: WorkflowInteractiveResponseType
): WorkflowInteractiveResponseType => {
if (interactive?.type === 'childrenInteractive' && interactive.params?.childrenResponse) {
if (
(interactive?.type === 'childrenInteractive' || interactive?.type === 'loopInteractive') &&
interactive.params?.childrenResponse
) {
return extractDeepestInteractive(interactive.params.childrenResponse);
}
return interactive;
@@ -40,6 +44,113 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
return limit * 2;
};
/* value type format */
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
const isObjectString = (value: any) => {
if (typeof value === 'string' && value !== 'false' && value !== 'true') {
const trimmedValue = value.trim();
const isJsonString =
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'));
return isJsonString;
}
return false;
};
// 1. any值忽略格式化
if (value === undefined || value === null) return value;
if (!type || type === WorkflowIOValueTypeEnum.any) return value;
// 2. 如果值已经符合目标类型,直接返回
if (
(type === WorkflowIOValueTypeEnum.string && typeof value === 'string') ||
(type === WorkflowIOValueTypeEnum.number && typeof value === 'number') ||
(type === WorkflowIOValueTypeEnum.boolean && typeof value === 'boolean') ||
(type.startsWith('array') && Array.isArray(value)) ||
(type === WorkflowIOValueTypeEnum.object && typeof value === 'object') ||
(type === WorkflowIOValueTypeEnum.chatHistory &&
(Array.isArray(value) || typeof value === 'number')) ||
(type === WorkflowIOValueTypeEnum.datasetQuote && Array.isArray(value)) ||
(type === WorkflowIOValueTypeEnum.selectDataset && Array.isArray(value)) ||
(type === WorkflowIOValueTypeEnum.selectApp && typeof value === 'object')
) {
return value;
}
// 4. 按目标类型,进行格式转化
// 4.1 基本类型转换
if (type === WorkflowIOValueTypeEnum.string) {
return typeof value === 'object' ? JSON.stringify(value) : String(value);
}
if (type === WorkflowIOValueTypeEnum.number) {
return Number(value);
}
if (type === WorkflowIOValueTypeEnum.boolean) {
if (typeof value === 'string') {
return value.toLowerCase() === 'true';
}
return Boolean(value);
}
// 4.3 字符串转对象
if (
(type === WorkflowIOValueTypeEnum.object || type.startsWith('array')) &&
typeof value === 'string' &&
value.trim()
) {
const trimmedValue = value.trim();
const isJsonString = isObjectString(trimmedValue);
if (isJsonString) {
try {
const parsed = json5.parse(trimmedValue);
// 检测解析结果与目标类型是否一致
if (type.startsWith('array') && Array.isArray(parsed)) return parsed;
if (type === WorkflowIOValueTypeEnum.object && typeof parsed === 'object') return parsed;
} catch (error) {}
}
}
// 4.4 数组类型(这里 value 不是数组类型)TODO: 嵌套数据类型转化)
if (type.startsWith('array')) {
return [value];
}
// 4.5 特殊类型处理
if (
[WorkflowIOValueTypeEnum.datasetQuote, WorkflowIOValueTypeEnum.selectDataset].includes(type)
) {
if (isObjectString(value)) {
try {
return json5.parse(value);
} catch (error) {
return [];
}
}
return [];
}
if (
[WorkflowIOValueTypeEnum.selectApp, WorkflowIOValueTypeEnum.object].includes(type) &&
typeof value === 'string'
) {
if (isObjectString(value)) {
try {
return json5.parse(value);
} catch (error) {
return {};
}
}
return {};
}
// Invalid history type
if (type === WorkflowIOValueTypeEnum.chatHistory) {
return 0;
}
// 5. 默认返回原值
return value;
};
/*
Get interaction information (if any) from the last AI message.
What can be done:
@@ -62,7 +173,10 @@ export const getLastInteractiveValue = (
return;
}
if (lastValue.interactive.type === 'childrenInteractive') {
if (
lastValue.interactive.type === 'childrenInteractive' ||
lastValue.interactive.type === 'loopInteractive'
) {
return lastValue.interactive;
}
@@ -83,7 +197,7 @@ export const getLastInteractiveValue = (
return;
};
export const initWorkflowEdgeStatus = (
export const storeEdges2RuntimeEdges = (
edges: StoreEdgeItemType[],
lastInteractive?: WorkflowInteractiveResponseType
): RuntimeEdgeItemType[] => {
@@ -114,7 +228,12 @@ export const getWorkflowEntryNodeIds = (
FlowNodeTypeEnum.pluginInput
];
return nodes
.filter((node) => entryList.includes(node.flowNodeType as any))
.filter(
(node) =>
entryList.includes(node.flowNodeType as any) ||
(!nodes.some((item) => entryList.includes(item.flowNodeType as any)) &&
node.flowNodeType === FlowNodeTypeEnum.tool)
)
.map((item) => item.nodeId);
};
@@ -312,7 +431,6 @@ export const formatVariableValByType = (val: any, valueType?: WorkflowIOValueTyp
if (
[
WorkflowIOValueTypeEnum.object,
WorkflowIOValueTypeEnum.chatHistory,
WorkflowIOValueTypeEnum.datasetQuote,
WorkflowIOValueTypeEnum.selectApp,
WorkflowIOValueTypeEnum.selectDataset

View File

@@ -34,6 +34,8 @@ import { LoopStartNode } from './system/loop/loopStart';
import { LoopEndNode } from './system/loop/loopEnd';
import { FormInputNode } from './system/interactive/formInput';
import { ToolParamsNode } from './system/toolParams';
import { RunToolNode } from './system/runTool';
import { RunToolSetNode } from './system/runToolSet';
const systemNodes: FlowNodeTemplateType[] = [
AiChatModule,
@@ -84,5 +86,7 @@ export const moduleTemplatesFlat: FlowNodeTemplateType[] = [
RunAppNode,
RunAppModule,
LoopStartNode,
LoopEndNode
LoopEndNode,
RunToolNode,
RunToolSetNode
];

View File

@@ -8,7 +8,7 @@ import { i18nT } from '../../../../web/i18n/utils';
export const Input_Template_History: FlowNodeInputItemType = {
key: NodeInputKeyEnum.history,
renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.chatHistory,
valueType: WorkflowIOValueTypeEnum.chatHistory, // Array / Number
label: i18nT('common:core.module.input.label.chat history'),
description: i18nT('workflow:max_dialog_rounds'),

View File

@@ -28,6 +28,15 @@ type ChildrenInteractive = InteractiveNodeType & {
};
};
type LoopInteractive = InteractiveNodeType & {
type: 'loopInteractive';
params: {
loopResult: any[];
childrenResponse: WorkflowInteractiveResponseType;
currentIndex: number;
};
};
export type UserSelectOptionItemType = {
key: string;
value: string;
@@ -71,5 +80,7 @@ type UserInputInteractive = InteractiveNodeType & {
export type InteractiveNodeResponseType =
| UserSelectInteractive
| UserInputInteractive
| ChildrenInteractive;
| ChildrenInteractive
| LoopInteractive;
export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType;

View File

@@ -0,0 +1,19 @@
import { FlowNodeTemplateTypeEnum } from '../../constants';
import { FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeTemplateType } from '../../type/node';
import { getHandleConfig } from '../utils';
export const RunToolNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.tool,
templateType: FlowNodeTemplateTypeEnum.other,
flowNodeType: FlowNodeTypeEnum.tool,
sourceHandle: getHandleConfig(true, true, true, true),
targetHandle: getHandleConfig(true, true, true, true),
intro: '',
name: '',
showStatus: false,
isTool: true,
version: '4.9.6',
inputs: [],
outputs: []
};

View File

@@ -0,0 +1,19 @@
import { FlowNodeTemplateTypeEnum } from '../../constants';
import { FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeTemplateType } from '../../type/node';
import { getHandleConfig } from '../utils';
export const RunToolSetNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.toolSet,
templateType: FlowNodeTemplateTypeEnum.other,
flowNodeType: FlowNodeTypeEnum.toolSet,
sourceHandle: getHandleConfig(false, false, false, false),
targetHandle: getHandleConfig(false, false, false, false),
intro: '',
name: '',
showStatus: false,
isTool: true,
version: '4.9.6',
inputs: [],
outputs: []
};

View File

@@ -311,6 +311,38 @@ export const appData2FlowNodeIO = ({
};
};
export const toolData2FlowNodeIO = ({
nodes
}: {
nodes: StoreNodeItemType[];
}): {
inputs: FlowNodeInputItemType[];
outputs: FlowNodeOutputItemType[];
} => {
const toolNode = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.tool);
return {
inputs: toolNode?.inputs || [],
outputs: toolNode?.outputs || []
};
};
export const toolSetData2FlowNodeIO = ({
nodes
}: {
nodes: StoreNodeItemType[];
}): {
inputs: FlowNodeInputItemType[];
outputs: FlowNodeOutputItemType[];
} => {
const toolSetNode = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet);
return {
inputs: toolSetNode?.inputs || [],
outputs: toolSetNode?.outputs || []
};
};
export const formatEditorVariablePickerIcon = (
variables: { key: string; label: string; type?: `${VariableInputEnum}`; required?: boolean }[]
): EditorVariablePickerType[] => {