Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
6ea65f64
authored
Mar 14, 2026
by
Archer
Committed by
GitHub
Mar 14, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix issue (#6560)
* perf: mcp json schema type * fix: workflow form value reset * fix: ts * fix: test
parent
dbc443a7
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
635 additions
and
320 deletions
+635
-320
.claude/CLAUDE.md
+4
-3
.claude/design/workflow-debug-form-input-bug-analysis.md
+0
-199
.claude/issue/ssrf-vulnerability-fix.md
+0
-0
.claude/issue/workflow-form-input-restore-bug.md
+343
-0
document/data/doc-last-modified.json
+1
-1
packages/global/common/system/constants.ts
+1
-0
packages/global/core/app/jsonschema.ts
+50
-9
packages/service/common/system/utils.ts
+3
-0
packages/service/core/app/mcp.ts
+17
-17
packages/service/core/workflow/dispatch/ai/extract.ts
+2
-7
packages/service/core/workflow/dispatch/ai/tool/toolCall.ts
+2
-12
packages/service/support/wallet/sub/utils.ts
+1
-1
packages/web/hooks/useConfirm.tsx
+3
-1
projects/app/src/components/core/app/FileSelector/index.tsx
+42
-14
projects/app/src/components/core/app/formRender/index.tsx
+1
-0
projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts
+15
-0
projects/app/src/components/core/chat/components/AIResponseBox.tsx
+1
-26
projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx
+2
-2
projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx
+29
-28
test/cases/global/core/app/jsonschema.test.ts
+118
-0
No files found.
.claude/CLAUDE.md
View file @
6ea65f64
...
@@ -5,10 +5,11 @@
...
@@ -5,10 +5,11 @@
## 输出要求
## 输出要求
1.
输出语言:中文
1.
输出语言:中文
2.
输出的设计文档位置:.claude/design,以 Markdown 文件为主。
2.
输出的设计文档位置:.claude/design,
问题分析文档位置: .claude/issue,
以 Markdown 文件为主。
3.
输出 Plan 时,均需写入 .claude/plan 目录下,以 Markdown 文件为主。
3.
输出 Plan 时,均需写入 .claude/plan 目录下,以 Markdown 文件为主。
4.
文件输出,使用正确的编码格式,例如UTF-8。
4.
相同需求文档,尽量写在一起,或者创建要给目录一起管理,不要随意平铺一堆不同版本的相同问题的文档。
5.
如果用户未指明,不要随意编写总结报告。
5.
文件输出,使用正确的编码格式,例如UTF-8。
6.
如果用户未指明,不要随意编写总结报告。
## 项目概述
## 项目概述
...
...
.claude/design/workflow-debug-form-input-bug-analysis.md
deleted
100644 → 0
View file @
dbc443a7
# 工作流调试弹窗表单输入内容清空问题分析
## 问题描述
**位置**
: 工作流画布右侧的 ChatTest 调试弹窗(运行测试对话窗口)
**前提**
: 包含表单输入节点(用户可输入内容)
**现象**
: 用户填写内容后提交,工作流继续运行。关闭调试弹窗后再打开,历史记录中的表单内容被清空
**预期**
: 内容不应被清空
## 数据流分析
### 正常流程
1.
**用户提交表单**
→
`AIResponseBox.tsx`
中的
`RenderUserFormInteractive`
组件
2.
**调用 handleFormSubmit**
→ 将表单数据 JSON 化并通过
`onSendPrompt`
发送
3.
**发送到后端**
→
`/api/core/chat/chatTest`
接收请求
4.
**工作流执行**
→
`dispatchWorkFlow`
处理表单输入节点
5.
**保存聊天记录**
→ 调用
`updateInteractiveChat`
更新数据库
6.
**更新 interactive**
→
`saveChat.ts`
中更新
`inputForm[].value`
7.
**关闭弹窗**
→ 调试状态保存
8.
**重新打开弹窗**
→ 从数据库读取聊天记录
9.
**渲染表单**
→
`RenderUserFormInteractive`
使用
`item.value ?? item.defaultValue`
### 关键代码位置
#### 1. 前端表单提交 (`AIResponseBox.tsx`)
```
typescript
// 第 231-237 行:计算 defaultValues
const
defaultValues
=
useMemo
(()
=>
{
return
interactive
.
params
.
inputForm
?.
reduce
((
acc
:
Record
<
string
,
any
>
,
item
,
index
)
=>
{
// 使用 ?? 运算符,只有 undefined 或 null 时才使用 defaultValue
acc
[
item
.
key
]
=
item
.
value
??
item
.
defaultValue
;
return
acc
;
},
{});
},
[
interactive
]);
```
#### 2. 后端保存逻辑 (`saveChat.ts`)
```
typescript
// 第 495-525 行:更新 inputForm 值
if
(
(
finalInteractive
.
type
===
'userInput'
||
finalInteractive
.
type
===
'agentPlanAskUserForm'
)
&&
typeof
parsedUserInteractiveVal
===
'object'
)
{
finalInteractive
.
params
.
inputForm
=
finalInteractive
.
params
.
inputForm
.
map
((
item
)
=>
{
const
itemValue
=
parsedUserInteractiveVal
[
item
.
key
];
if
(
itemValue
===
undefined
)
return
item
;
return
{
...
item
,
value
:
itemValue
// ✅ 保存用户输入的值
};
});
finalInteractive
.
params
.
submitted
=
true
;
// ✅ 标记为已提交
}
// 第 533 行:将更新后的 interactive 赋值给最后一条消息
chatItem
.
value
[
chatItem
.
value
.
length
-
1
].
interactive
=
interactive
;
```
#### 3. API 调用 (`chatTest.ts`)
```
typescript
// 第 263-267 行:根据是否有 interactive 选择保存方式
if
(
interactive
)
{
await
updateInteractiveChat
({
interactive
,
...
params
});
}
else
{
await
pushChatRecords
(
params
);
}
```
## 问题排查
需要验证以下几点:
1.
**后端是否正确保存了 `inputForm[].value`?**
-
检查数据库中的
`chat_items`
集合
-
查看
`value`
字段中的
`interactive.params.inputForm`
是否包含用户提交的值
2.
**前端是否正确读取了保存的值?**
-
检查
`getChatRecords`
API 返回的数据
-
查看
`interactive.params.inputForm[].value`
是否存在
3.
**是否有其他地方覆盖了 `interactive` 数据?**
-
检查是否有缓存或状态管理覆盖了数据库的值
## 调试步骤
### 1. 检查数据库保存
在
`saveChat.ts`
的
`updateInteractiveChat`
函数中添加日志:
```
typescript
// 第 498 行之后
finalInteractive
.
params
.
inputForm
=
finalInteractive
.
params
.
inputForm
.
map
((
item
)
=>
{
const
itemValue
=
parsedUserInteractiveVal
[
item
.
key
];
if
(
itemValue
===
undefined
)
return
item
;
console
.
log
(
'Saving form value:'
,
{
key
:
item
.
key
,
value
:
itemValue
});
// 添加日志
return
{
...
item
,
value
:
itemValue
};
});
```
### 2. 检查 API 返回数据
在
`AIResponseBox.tsx`
中添加日志:
```
typescript
// 第 231 行之后
const
defaultValues
=
useMemo
(()
=>
{
console
.
log
(
'Interactive data:'
,
interactive
);
// 添加日志
console
.
log
(
'InputForm:'
,
interactive
.
params
.
inputForm
);
// 添加日志
return
interactive
.
params
.
inputForm
?.
reduce
((
acc
:
Record
<
string
,
any
>
,
item
,
index
)
=>
{
console
.
log
(
'Form item:'
,
{
key
:
item
.
key
,
value
:
item
.
value
,
defaultValue
:
item
.
defaultValue
});
// 添加日志
acc
[
item
.
key
]
=
item
.
value
??
item
.
defaultValue
;
return
acc
;
},
{});
},
[
interactive
]);
```
### 3. 检查数据库记录
直接查询 MongoDB:
```
javascript
db
.
chat_items
.
find
({
chatId
:
"your_chat_id"
,
obj
:
"AI"
}).
sort
({
_id
:
-
1
}).
limit
(
1
)
```
查看返回的
`value`
字段中的
`interactive.params.inputForm`
是否包含
`value`
属性。
## 可能的原因
### 原因 1: 数据库未正确保存
如果
`updateInteractiveChat`
没有被正确调用,或者保存失败,数据库中就不会有用户提交的值。
**验证方法**
: 检查数据库记录
### 原因 2: API 返回数据不完整
如果
`getChatRecords`
API 没有返回完整的
`interactive`
数据,前端就无法显示用户提交的值。
**验证方法**
: 检查 API 响应
### 原因 3: 前端状态管理问题
如果前端有缓存或状态管理覆盖了数据库的值,也会导致表单内容被清空。
**验证方法**
: 检查 React 组件的 props 和 state
## 临时解决方案
如果问题是由于数据库未正确保存导致的,可以使用
`sessionStorage`
作为临时方案:
```
typescript
// 在 AIResponseBox.tsx 的 defaultValues 计算中
const
defaultValues
=
useMemo
(()
=>
{
// 尝试从 sessionStorage 恢复数据
let
savedData
:
Record
<
string
,
any
>
=
{};
if
(
typeof
window
!==
'undefined'
)
{
try
{
const
saved
=
sessionStorage
.
getItem
(
`interactiveForm_
${
chatItemDataId
}
`
);
if
(
saved
)
{
savedData
=
JSON
.
parse
(
saved
);
}
}
catch
(
error
)
{
console
.
warn
(
'Failed to parse saved form data:'
,
error
);
}
}
return
interactive
.
params
.
inputForm
?.
reduce
((
acc
:
Record
<
string
,
any
>
,
item
,
index
)
=>
{
// 优先使用 item.value,其次使用 sessionStorage,最后使用 defaultValue
acc
[
item
.
key
]
=
item
.
value
??
savedData
[
item
.
key
]
??
item
.
defaultValue
;
return
acc
;
},
{});
},
[
interactive
,
chatItemDataId
]);
```
但这只是临时方案,根本问题还是需要确保数据库正确保存了用户提交的值。
## 下一步
1.
添加日志验证数据流
2.
检查数据库记录
3.
根据调试结果确定具体原因
4.
实施修复方案
.claude/
design
/ssrf-vulnerability-fix.md
→
.claude/
issue
/ssrf-vulnerability-fix.md
View file @
6ea65f64
File moved
.claude/issue/workflow-form-input-restore-bug.md
0 → 100644
View file @
6ea65f64
# 工作流表单输入节点重新打开预览页面后表单内容恢复默认值问题
## 问题描述
在工作流中添加表单输入节点后,在运行预览页面进行对话测试时:
1.
触发表单输入交互
2.
正常填写表单并提交
3.
任务继续运行成功
4.
**关闭预览页面**
5.
**重新打开预览页面**
6.
**问题**
: 表单内容被恢复为默认值,而不是用户之前填写的值
## 根本原因分析
### 1. 数据结构设计
根据类型定义
`packages/global/core/workflow/template/system/interactive/type.ts`
:
```
typescript
export
type
UserInputFormItemType
=
{
key
:
string
;
label
:
string
;
value
:
any
;
// 用户填写的值
defaultValue
?:
any
;
// 默认值
required
:
boolean
;
// ...
}
export
type
UserInputInteractive
=
{
type
:
'userInput'
;
params
:
{
description
:
string
;
inputForm
:
UserInputFormItemType
[];
submitted
?:
boolean
;
// 是否已提交
}
}
```
**设计意图**
:
-
`value`
字段用于存储用户填写的值
-
`submitted`
标记表单是否已提交
-
这些数据应该保存在聊天记录的
`interactive`
对象中
### 2. 实际实现的问题
#### 问题 1: sessionStorage 的冗余使用
在
`AIResponseBox.tsx`
的
`RenderUserFormInteractive`
组件中(第 248-271 行):
```
typescript
if
(
typeof
window
!==
'undefined'
)
{
const
dataToSave
=
{
...
data
};
// ... 处理文件数据
sessionStorage
.
setItem
(
`interactiveForm_
${
chatItemDataId
}
`
,
JSON
.
stringify
(
dataToSave
));
}
```
**问题**
:
-
表单提交时保存到
`sessionStorage`
,但
**从未读取**
-
通过全局搜索确认: 只有写入,没有任何读取操作
-
这是一个
**无效的代码**
,增加了复杂度但没有实际作用
#### 问题 2: defaultValues 计算逻辑不完整
在
`RenderUserFormInteractive`
组件中(第 231-237 行):
```
typescript
const
defaultValues
=
useMemo
(()
=>
{
return
interactive
.
params
.
inputForm
?.
reduce
((
acc
:
Record
<
string
,
any
>
,
item
)
=>
{
acc
[
item
.
key
]
=
item
.
value
??
item
.
defaultValue
;
return
acc
;
},
{});
},
[
interactive
]);
```
**逻辑**
:
`item.value`
优先于
`item.defaultValue`
**问题**
: 当页面重新打开时,
`interactive.params`
从聊天记录中恢复,但:
-
如果后端没有正确保存用户填写的
`value`
到
`interactive.params.inputForm`
-
或者前端没有正确更新
`interactive`
对象
-
就会导致
`item.value`
为空,回退到
`defaultValue`
### 3. 数据流分析
**正常流程(应该是这样)**
:
```
用户填写表单
→ 提交时发送到后端
→ 后端更新 interactive.params.inputForm[].value
→ 后端保存到聊天记录
→ 关闭预览页面
→ 重新打开预览页面
→ 从聊天记录恢复 interactive
→ defaultValues 从 item.value 读取
→ 表单显示用户填写的值 ✅
```
**实际流程(出问题了)**
:
```
用户填写表单
→ 提交时发送到后端
→ 后端处理但可能没有更新 interactive.params.inputForm[].value
→ 或者前端没有正确更新本地的 interactive 对象
→ 关闭预览页面
→ 重新打开预览页面
→ 从聊天记录恢复 interactive
→ interactive.params.inputForm[].value 为空
→ defaultValues 回退到 item.defaultValue
→ 表单显示默认值 ❌
```
### 4. 核心问题定位 ✅
**问题确认**
: 前端在表单提交后,只更新了
`submitted: true`
,但
**没有更新 `inputForm[].value`**
在
`projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts`
的
`rewriteHistoriesByInteractiveResponse`
函数中(第 154-168 行):
```
typescript
if
(
finalInteractive
.
type
===
'userInput'
||
finalInteractive
.
type
===
'agentPlanAskUserForm'
)
{
return
{
...
val
,
interactive
:
{
...
finalInteractive
,
params
:
{
...
finalInteractive
.
params
,
submitted
:
true
// ✅ 只设置了 submitted
// ❌ 但没有更新 inputForm[].value
}
}
};
}
```
**分析**
:
-
用户提交的表单数据在
`interactiveVal`
参数中(JSON 字符串格式)
-
函数只是简单地标记
`submitted: true`
-
没有解析
`interactiveVal`
并更新
`params.inputForm[].value`
-
导致重新打开页面时,
`item.value`
仍然是空的,回退到
`defaultValue`
## 重要发现: sessionStorage 的设计意图
经过深入分析,发现
`sessionStorage`
的使用
**可能有其合理性**
:
### chatItemDataId 的含义
-
`chatItemDataId`
是
**每条聊天消息的唯一标识**
(不是 chatId)
-
一个对话(chatId)中可能有
**多条消息**
,每条消息有不同的
`dataId`
-
一个工作流中可能有
**多个表单输入节点**
,每个节点触发时会创建新的消息
### 可能的场景
**场景 1: 同一对话中多个表单输入**
```
对话开始
→ 触发表单输入节点 A (dataId: xxx-1)
→ 用户填写表单 A
→ 提交,继续执行
→ 触发表单输入节点 B (dataId: xxx-2)
→ 用户填写表单 B
→ 关闭预览页面
→ 重新打开
→ 需要恢复两个表单的数据
```
**场景 2: 表单数据的临时性**
-
用户可能在填写过程中关闭页面(未提交)
-
sessionStorage 可以保存
**未提交的草稿**
-
重新打开时恢复草稿,避免用户重新填写
### 为什么后端保存不够?
1.
**未提交的数据**
: 用户填写了一半但未提交,后端没有这些数据
2.
**多个表单实例**
: 同一对话中可能有多个表单输入节点,需要分别保存
3.
**临时状态**
: 表单的临时编辑状态(如文件上传中)不应该保存到后端
## 影响范围
-
**影响文件**
:
-
`projects/app/src/components/core/chat/components/AIResponseBox.tsx`
-
`projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts`
-
**影响组件**
:
`RenderUserFormInteractive`
,
`rewriteHistoriesByInteractiveResponse`
-
**影响场景**
:
-
所有使用表单输入节点的工作流
-
在预览页面关闭后重新打开时
-
同一对话中有多个表单输入节点时
## 解决方案(修正版)
### 方案 1: 双重保存机制 - sessionStorage + interactive.params (推荐)
结合两种机制的优点:
-
**sessionStorage**
: 保存未提交的草稿和临时状态
-
**interactive.params**
: 保存已提交的最终数据
#### 步骤 1: 修复 `rewriteHistoriesByInteractiveResponse` (已提交数据)
**文件**
:
`projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts`
```
typescript
if
(
finalInteractive
.
type
===
'userInput'
||
finalInteractive
.
type
===
'agentPlanAskUserForm'
)
{
// 解析用户提交的表单数据
let
submittedData
:
Record
<
string
,
any
>
=
{};
try
{
submittedData
=
JSON
.
parse
(
interactiveVal
);
}
catch
(
error
)
{
console
.
warn
(
'Failed to parse form input data'
,
error
);
}
// 更新 inputForm 中的 value
const
updatedInputForm
=
finalInteractive
.
params
.
inputForm
.
map
((
item
)
=>
({
...
item
,
value
:
submittedData
[
item
.
key
]
??
item
.
value
??
item
.
defaultValue
}));
return
{
...
val
,
interactive
:
{
...
finalInteractive
,
params
:
{
...
finalInteractive
.
params
,
inputForm
:
updatedInputForm
,
submitted
:
true
}
}
};
}
```
#### 步骤 2: 修复 `defaultValues` 计算逻辑 (恢复草稿)
**文件**
:
`projects/app/src/components/core/chat/components/AIResponseBox.tsx`
```
typescript
const
defaultValues
=
useMemo
(()
=>
{
// 1. 优先从 sessionStorage 恢复数据(包括未提交的草稿)
let
savedData
:
Record
<
string
,
any
>
|
null
=
null
;
if
(
typeof
window
!==
'undefined'
)
{
try
{
const
saved
=
sessionStorage
.
getItem
(
`interactiveForm_
${
chatItemDataId
}
`
);
if
(
saved
)
{
savedData
=
JSON
.
parse
(
saved
);
}
}
catch
(
error
)
{
console
.
warn
(
'Failed to restore form data from sessionStorage'
,
error
);
}
}
// 2. 构建 defaultValues
// 优先级: sessionStorage(草稿) > item.value(已提交) > item.defaultValue(默认)
return
interactive
.
params
.
inputForm
?.
reduce
((
acc
:
Record
<
string
,
any
>
,
item
)
=>
{
if
(
savedData
&&
item
.
key
in
savedData
)
{
// 优先使用 sessionStorage 中的数据(可能是未提交的草稿)
acc
[
item
.
key
]
=
savedData
[
item
.
key
];
}
else
{
// 否则使用 item.value(已提交的数据) 或 defaultValue
acc
[
item
.
key
]
=
item
.
value
??
item
.
defaultValue
;
}
return
acc
;
},
{});
},
[
interactive
,
chatItemDataId
]);
```
#### 步骤 3: 清理 sessionStorage (可选优化)
在表单提交成功后,清理对应的 sessionStorage:
```
typescript
const
handleFormSubmit
=
useCallback
(
(
data
:
Record
<
string
,
any
>
)
=>
{
const
finalData
:
Record
<
string
,
any
>
=
{};
interactive
.
params
.
inputForm
?.
forEach
((
item
)
=>
{
if
(
item
.
key
in
data
)
{
finalData
[
item
.
key
]
=
data
[
item
.
key
];
}
});
// 保存到 sessionStorage (用于页面关闭后恢复)
if
(
typeof
window
!==
'undefined'
)
{
const
dataToSave
=
{
...
data
};
// ... 处理文件数据
sessionStorage
.
setItem
(
`interactiveForm_
${
chatItemDataId
}
`
,
JSON
.
stringify
(
dataToSave
));
}
onSendPrompt
(
JSON
.
stringify
(
finalData
));
// 可选: 提交成功后清理 sessionStorage
// setTimeout(() => {
// sessionStorage.removeItem(`interactiveForm_${chatItemDataId}`);
// }, 1000);
},
[
chatItemDataId
,
interactive
.
params
.
inputForm
]
);
```
**优点**
:
-
保留 sessionStorage 的草稿保存功能
-
同时修复已提交数据的持久化问题
-
支持多个表单输入节点的场景
-
向后兼容
**缺点**
:
-
需要修改两个地方
-
逻辑稍微复杂一些
### 方案 2: 仅修复 interactive.params (简化方案)
如果不需要草稿保存功能,可以只修复
`rewriteHistoriesByInteractiveResponse`
,删除 sessionStorage 相关代码。
**优点**
: 简单,代码更清晰
**缺点**
: 失去草稿保存功能
## 推荐实施方案
**推荐方案 1**
,原因:
1.
保留了 sessionStorage 的设计意图(草稿保存)
2.
修复了已提交数据的持久化问题
3.
支持复杂场景(多个表单、未提交草稿)
4.
向后兼容,不破坏现有功能
## 相关文件
-
`projects/app/src/components/core/chat/components/AIResponseBox.tsx`
- 表单渲染和提交逻辑
-
`projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx`
- 表单输入组件
-
`projects/app/src/web/core/chat/context/chatItemContext.tsx`
- Chat 上下文管理
-
`packages/service/core/workflow/dispatch/interactive/formInput.ts`
- 后端表单输入处理
## 测试建议
修复后需要测试以下场景:
1.
**基本场景**
: 填写表单 → 提交 → 关闭预览 → 重新打开 → 验证表单内容保持
2.
**多次提交**
: 填写 → 提交 → 修改 → 再次提交 → 关闭 → 重新打开 → 验证最后一次提交的内容
3.
**文件上传**
: 包含文件选择的表单,验证文件信息正确恢复
4.
**必填项验证**
: 验证必填项的验证逻辑不受影响
5.
**多个表单**
: 同一对话中多个表单输入节点,验证各自独立保存和恢复
6.
**清空对话**
: 点击"重新开始"后,验证表单数据被正确清空
document/data/doc-last-modified.json
View file @
6ea65f64
...
@@ -235,7 +235,7 @@
...
@@ -235,7 +235,7 @@
"document/content/docs/self-host/upgrading/4-14/4148.mdx"
:
"2026-03-09T17:39:53+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4148.mdx"
:
"2026-03-09T17:39:53+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41481.en.mdx"
:
"2026-03-09T12:02:02+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41481.en.mdx"
:
"2026-03-09T12:02:02+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41481.mdx"
:
"2026-03-09T17:39:53+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41481.mdx"
:
"2026-03-09T17:39:53+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4149.mdx"
:
"2026-03-1
2T00:15:29
+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4149.mdx"
:
"2026-03-1
4T22:07:04
+08:00"
,
"document/content/docs/self-host/upgrading/outdated/40.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/outdated/40.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/outdated/40.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/outdated/40.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/outdated/41.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/outdated/41.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
...
...
packages/global/common/system/constants.ts
View file @
6ea65f64
...
@@ -6,5 +6,6 @@ export const DEFAULT_TEAM_AVATAR = `/imgs/avatar/defaultTeamAvatar.svg`;
...
@@ -6,5 +6,6 @@ export const DEFAULT_TEAM_AVATAR = `/imgs/avatar/defaultTeamAvatar.svg`;
export
const
DEFAULT_ORG_AVATAR
=
'/imgs/avatar/defaultOrgAvatar.svg'
;
export
const
DEFAULT_ORG_AVATAR
=
'/imgs/avatar/defaultOrgAvatar.svg'
;
export
const
DEFAULT_USER_AVATAR
=
'/imgs/avatar/BlueAvatar.svg'
;
export
const
DEFAULT_USER_AVATAR
=
'/imgs/avatar/BlueAvatar.svg'
;
export
const
isDevEnv
=
process
.
env
.
NODE_ENV
===
'development'
;
export
const
isProduction
=
process
.
env
.
NODE_ENV
===
'production'
;
export
const
isProduction
=
process
.
env
.
NODE_ENV
===
'production'
;
export
const
isTestEnv
=
process
.
env
.
NODE_ENV
===
'test'
;
export
const
isTestEnv
=
process
.
env
.
NODE_ENV
===
'test'
;
packages/global/core/app/jsonschema.ts
View file @
6ea65f64
...
@@ -9,13 +9,51 @@ import { i18nT } from '../../../web/i18n/utils';
...
@@ -9,13 +9,51 @@ import { i18nT } from '../../../web/i18n/utils';
import
z
from
'zod'
;
import
z
from
'zod'
;
export
const
JsonSchemaPropertiesItemSchema
=
z
.
object
({
export
const
JsonSchemaPropertiesItemSchema
=
z
.
object
({
description
:
z
.
string
().
optional
(),
// 基本类型定义
'x-tool-description'
:
z
.
string
().
optional
(),
type
:
z
.
any
().
optional
(),
// 可能不存在(使用 anyOf/oneOf 时)
type
:
z
.
any
(),
enum
:
z
.
array
(
z
.
string
()).
optional
(),
// 组合类型(JSON Schema 规范)
minimum
:
z
.
number
().
optional
(),
anyOf
:
z
.
array
(
z
.
any
()).
optional
(),
// 任意一个匹配(联合类型,如 Optional[T])
maximum
:
z
.
number
().
optional
(),
oneOf
:
z
.
array
(
z
.
any
()).
optional
(),
// 只能匹配一个
items
:
z
.
any
().
optional
()
// Array 时候有
allOf
:
z
.
array
(
z
.
any
()).
optional
(),
// 必须全部匹配
not
:
z
.
any
().
optional
(),
// 不匹配
// 枚举和常量
enum
:
z
.
array
(
z
.
string
()).
optional
(),
// 枚举值
const
:
z
.
any
().
optional
(),
// 常量值
// 字符串约束
minLength
:
z
.
number
().
optional
(),
// 最小长度
maxLength
:
z
.
number
().
optional
(),
// 最大长度
pattern
:
z
.
string
().
optional
(),
// 正则表达式
format
:
z
.
string
().
optional
(),
// 格式(email, uri, date-time 等)
// 数字约束
minimum
:
z
.
number
().
optional
(),
// 最小值
maximum
:
z
.
number
().
optional
(),
// 最大值
exclusiveMinimum
:
z
.
union
([
z
.
number
(),
z
.
boolean
()]).
optional
(),
// 排他最小值
exclusiveMaximum
:
z
.
union
([
z
.
number
(),
z
.
boolean
()]).
optional
(),
// 排他最大值
multipleOf
:
z
.
number
().
optional
(),
// 倍数
// 数组约束
items
:
z
.
any
().
optional
(),
// 数组项类型
minItems
:
z
.
number
().
optional
(),
// 最小项数
maxItems
:
z
.
number
().
optional
(),
// 最大项数
uniqueItems
:
z
.
boolean
().
optional
(),
// 唯一项
// 对象约束
properties
:
z
.
record
(
z
.
string
(),
z
.
any
()).
optional
(),
// 对象属性
required
:
z
.
array
(
z
.
string
()).
optional
(),
// 必填字段
additionalProperties
:
z
.
union
([
z
.
boolean
(),
z
.
any
()]).
optional
(),
// 额外属性
// 元数据
title
:
z
.
string
().
optional
(),
// 标题
description
:
z
.
string
().
optional
(),
// 描述
default
:
z
.
any
().
optional
(),
// 默认值
examples
:
z
.
array
(
z
.
any
()).
optional
(),
// 示例
// 自定义扩展(FastGPT 专用)
'x-tool-description'
:
z
.
string
().
optional
()
// 工具描述
});
});
export
type
JsonSchemaPropertiesItemType
=
z
.
infer
<
typeof
JsonSchemaPropertiesItemSchema
>
;
export
type
JsonSchemaPropertiesItemType
=
z
.
infer
<
typeof
JsonSchemaPropertiesItemSchema
>
;
...
@@ -37,16 +75,19 @@ export const getNodeInputTypeFromSchemaInputType = ({
...
@@ -37,16 +75,19 @@ export const getNodeInputTypeFromSchemaInputType = ({
type
,
type
,
arrayItems
arrayItems
}:
{
}:
{
type
:
string
;
type
:
string
|
undefined
;
arrayItems
?:
{
type
:
string
};
arrayItems
?:
{
type
:
string
};
})
=>
{
})
=>
{
// 如果 type 为 undefined,返回 any 类型(处理 anyOf/oneOf 等联合类型)
if
(
!
type
)
return
WorkflowIOValueTypeEnum
.
any
;
if
(
type
===
'string'
)
return
WorkflowIOValueTypeEnum
.
string
;
if
(
type
===
'string'
)
return
WorkflowIOValueTypeEnum
.
string
;
if
(
type
===
'number'
||
type
===
'integer'
)
return
WorkflowIOValueTypeEnum
.
number
;
if
(
type
===
'number'
||
type
===
'integer'
)
return
WorkflowIOValueTypeEnum
.
number
;
if
(
type
===
'boolean'
)
return
WorkflowIOValueTypeEnum
.
boolean
;
if
(
type
===
'boolean'
)
return
WorkflowIOValueTypeEnum
.
boolean
;
if
(
type
===
'object'
)
return
WorkflowIOValueTypeEnum
.
object
;
if
(
type
===
'object'
)
return
WorkflowIOValueTypeEnum
.
object
;
// Array
if
(
type
!==
'array'
)
return
WorkflowIOValueTypeEnum
.
any
;
if
(
type
!==
'array'
)
return
WorkflowIOValueTypeEnum
.
any
;
if
(
!
arrayItems
)
return
WorkflowIOValueTypeEnum
.
arrayAny
;
if
(
!
arrayItems
)
return
WorkflowIOValueTypeEnum
.
arrayAny
;
const
itemType
=
arrayItems
.
type
;
const
itemType
=
arrayItems
.
type
;
...
...
packages/service/common/system/utils.ts
View file @
6ea65f64
import
{
isIP
}
from
'net'
;
import
{
isIP
}
from
'net'
;
import
*
as
dns
from
'node:dns/promises'
;
import
*
as
dns
from
'node:dns/promises'
;
import
{
SERVICE_LOCAL_HOST
}
from
'./tools'
;
import
{
SERVICE_LOCAL_HOST
}
from
'./tools'
;
import
{
isDevEnv
}
from
'@fastgpt/global/common/system/constants'
;
export
const
isInternalAddress
=
async
(
url
:
string
):
Promise
<
boolean
>
=>
{
export
const
isInternalAddress
=
async
(
url
:
string
):
Promise
<
boolean
>
=>
{
if
(
isDevEnv
)
return
false
;
const
isInternalIPv6
=
(
ip
:
string
):
boolean
=>
{
const
isInternalIPv6
=
(
ip
:
string
):
boolean
=>
{
// 移除 IPv6 地址中的方括号(如果有)
// 移除 IPv6 地址中的方括号(如果有)
const
cleanIp
=
ip
.
replace
(
/^
\[
|
\]
$/g
,
''
);
const
cleanIp
=
ip
.
replace
(
/^
\[
|
\]
$/g
,
''
);
...
...
packages/service/core/app/mcp.ts
View file @
6ea65f64
...
@@ -120,24 +120,24 @@ export class MCPClient {
...
@@ -120,24 +120,24 @@ export class MCPClient {
const
tools
=
await
Promise
.
all
(
const
tools
=
await
Promise
.
all
(
response
.
tools
.
map
(
async
(
tool
)
=>
{
response
.
tools
.
map
(
async
(
tool
)
=>
{
let
processedSchema
;
const
processedSchema
=
await
(
async
()
=>
{
if
(
tool
.
inputSchema
)
{
if
(
tool
.
inputSchema
)
{
try
{
try
{
// Deep clone to avoid dereference() mutating the original object
// Deep clone to avoid dereference() mutating the original object
const
schemaClone
=
JSON
.
parse
(
JSON
.
stringify
(
tool
.
inputSchema
));
const
schemaClone
=
JSON
.
parse
(
JSON
.
stringify
(
tool
.
inputSchema
));
return
await
$RefParser
.
dereference
(
schemaClone
,
{
processedSchema
=
await
$RefParser
.
dereference
(
schemaClone
,
{
resolve
:
{
resolve
:
{
// Disable file and HTTP $ref resolution to prevent SSRF
// Disable file and HTTP $ref resolution to prevent SSRF
file
:
false
,
file
:
false
,
http
:
false
http
:
false
}
}
}
);
}
);
}
catch
(
error
)
{
}
catch
(
error
)
{
logger
.
error
(
`Failed to dereference schema for tool "
${
tool
.
name
}
":`
,
{
error
});
logger
.
error
(
`Failed to dereference schema for tool "
${
tool
.
name
}
":`
,
{
error
})
;
return
tool
.
inputSchema
;
processedSchema
=
tool
.
inputSchema
;
}
}
}
}
}
)();
return
{
return
{
name
:
tool
.
name
,
name
:
tool
.
name
,
...
...
packages/service/core/workflow/dispatch/ai/extract.ts
View file @
6ea65f64
...
@@ -31,6 +31,7 @@ import {
...
@@ -31,6 +31,7 @@ import {
getExtractJsonToolPrompt
getExtractJsonToolPrompt
}
from
'@fastgpt/global/core/ai/prompt/agent'
;
}
from
'@fastgpt/global/core/ai/prompt/agent'
;
import
{
createLLMResponse
}
from
'../../../ai/llm/request'
;
import
{
createLLMResponse
}
from
'../../../ai/llm/request'
;
import
type
{
JsonSchemaPropertiesItemType
}
from
'@fastgpt/global/core/app/jsonschema'
;
type
Props
=
ModuleDispatchProps
<
{
type
Props
=
ModuleDispatchProps
<
{
[
NodeInputKeyEnum
.
history
]?:
ChatItemType
[];
[
NodeInputKeyEnum
.
history
]?:
ChatItemType
[];
...
@@ -162,13 +163,7 @@ export async function dispatchContentExtract(props: Props): Promise<Response> {
...
@@ -162,13 +163,7 @@ export async function dispatchContentExtract(props: Props): Promise<Response> {
}
}
const
getJsonSchema
=
({
params
:
{
extractKeys
}
}:
ActionProps
)
=>
{
const
getJsonSchema
=
({
params
:
{
extractKeys
}
}:
ActionProps
)
=>
{
const
properties
:
Record
<
const
properties
:
Record
<
string
,
JsonSchemaPropertiesItemType
>
=
{};
string
,
{
type
:
string
;
description
:
string
;
}
>
=
{};
extractKeys
.
forEach
((
item
)
=>
{
extractKeys
.
forEach
((
item
)
=>
{
const
jsonSchema
=
item
.
valueType
const
jsonSchema
=
item
.
valueType
?
valueTypeJsonSchemaMap
[
item
.
valueType
]
||
toolValueTypeList
[
0
].
jsonSchema
?
valueTypeJsonSchemaMap
[
item
.
valueType
]
||
toolValueTypeList
[
0
].
jsonSchema
...
...
packages/service/core/workflow/dispatch/ai/tool/toolCall.ts
View file @
6ea65f64
...
@@ -17,6 +17,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
...
@@ -17,6 +17,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import
{
toolValueTypeList
,
valueTypeJsonSchemaMap
}
from
'@fastgpt/global/core/workflow/constants'
;
import
{
toolValueTypeList
,
valueTypeJsonSchemaMap
}
from
'@fastgpt/global/core/workflow/constants'
;
import
{
runAgentCall
}
from
'../../../../ai/llm/agentCall'
;
import
{
runAgentCall
}
from
'../../../../ai/llm/agentCall'
;
import
type
{
ToolCallChildrenInteractive
}
from
'@fastgpt/global/core/workflow/template/system/interactive/type'
;
import
type
{
ToolCallChildrenInteractive
}
from
'@fastgpt/global/core/workflow/template/system/interactive/type'
;
import
type
{
JsonSchemaPropertiesItemType
}
from
'@fastgpt/global/core/app/jsonschema'
;
type
ResponseType
=
{
type
ResponseType
=
{
requestIds
:
string
[];
requestIds
:
string
[];
...
@@ -70,18 +71,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
...
@@ -70,18 +71,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
};
};
}
}
const
properties
:
Record
<
const
properties
:
Record
<
string
,
JsonSchemaPropertiesItemType
>
=
{};
string
,
{
type
:
string
;
description
:
string
;
enum
?:
string
[];
required
?:
boolean
;
items
?:
{
type
:
string
;
};
}
>
=
{};
item
.
toolParams
.
forEach
((
item
)
=>
{
item
.
toolParams
.
forEach
((
item
)
=>
{
const
jsonSchema
=
item
.
valueType
const
jsonSchema
=
item
.
valueType
?
valueTypeJsonSchemaMap
[
item
.
valueType
]
||
toolValueTypeList
[
0
].
jsonSchema
?
valueTypeJsonSchemaMap
[
item
.
valueType
]
||
toolValueTypeList
[
0
].
jsonSchema
...
...
packages/service/support/wallet/sub/utils.ts
View file @
6ea65f64
...
@@ -242,7 +242,7 @@ export const getTeamPlanStatus = async ({
...
@@ -242,7 +242,7 @@ export const getTeamPlanStatus = async ({
teamPoint
.
updateTeamPointsCache
({
teamId
,
totalPoints
,
surplusPoints
});
teamPoint
.
updateTeamPointsCache
({
teamId
,
totalPoints
,
surplusPoints
});
return
{
return
{
[
SubTypeEnum
.
standard
]
:
standard
:
standardPlan
.
currentSubLevel
===
StandardSubLevelEnum
.
custom
&&
standardConstants
standardPlan
.
currentSubLevel
===
StandardSubLevelEnum
.
custom
&&
standardConstants
?
{
?
{
...
standardPlan
,
...
standardPlan
,
...
...
packages/web/hooks/useConfirm.tsx
View file @
6ea65f64
...
@@ -124,7 +124,8 @@ export const useConfirm = (props?: {
...
@@ -124,7 +124,8 @@ export const useConfirm = (props?: {
const
isInputDeleteConfirmValid
=
!
isInputDelete
const
isInputDeleteConfirmValid
=
!
isInputDelete
?
true
?
true
:
!!
customContentInputConfirmText
&&
inputValue
.
trim
()
===
customContentInputConfirmText
;
:
!!
customContentInputConfirmText
&&
inputValue
.
trim
()
===
customContentInputConfirmText
.
trim
();
return
(
return
(
<
MyModal
<
MyModal
...
@@ -150,6 +151,7 @@ export const useConfirm = (props?: {
...
@@ -150,6 +151,7 @@ export const useConfirm = (props?: {
<
Input
<
Input
size=
{
'sm'
}
size=
{
'sm'
}
value=
{
inputValue
}
value=
{
inputValue
}
autoFocus
onChange=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
placeholder=
{
t
(
'common:confirm_input_delete_placeholder'
,
{
placeholder=
{
t
(
'common:confirm_input_delete_placeholder'
,
{
confirmText
:
customContentInputConfirmText
confirmText
:
customContentInputConfirmText
...
...
projects/app/src/components/core/app/FileSelector/index.tsx
View file @
6ea65f64
...
@@ -44,13 +44,15 @@ const FileSelector = ({
...
@@ -44,13 +44,15 @@ const FileSelector = ({
customFileExtensionList
,
customFileExtensionList
,
canLocalUpload
,
canLocalUpload
,
canUrlUpload
,
canUrlUpload
,
isDisabled
=
false
isDisabled
=
false
,
isInvalid
=
false
}:
AppFileSelectConfigType
&
{
}:
AppFileSelectConfigType
&
{
value
:
UserInputFileItemType
[];
value
:
UserInputFileItemType
[];
onChange
:
(
e
:
any
[])
=>
void
;
onChange
:
(
e
:
any
[])
=>
void
;
canLocalUpload
?:
boolean
;
canLocalUpload
?:
boolean
;
canUrlUpload
?:
boolean
;
canUrlUpload
?:
boolean
;
isDisabled
?:
boolean
;
isDisabled
?:
boolean
;
isInvalid
?:
boolean
;
})
=>
{
})
=>
{
const
{
feConfigs
}
=
useSystemStore
();
const
{
feConfigs
}
=
useSystemStore
();
const
{
teamPlanStatus
}
=
useUserStore
();
const
{
teamPlanStatus
}
=
useUserStore
();
...
@@ -382,7 +384,7 @@ const FileSelector = ({
...
@@ -382,7 +384,7 @@ const FileSelector = ({
px=
{
3
}
px=
{
3
}
py=
{
[
4
,
7
]
}
py=
{
[
4
,
7
]
}
border=
{
'1.5px dashed'
}
border=
{
'1.5px dashed'
}
borderColor=
{
'myGray.250'
}
borderColor=
{
isInvalid
?
'red.500'
:
'myGray.250'
}
borderRadius=
{
'md'
}
borderRadius=
{
'md'
}
userSelect=
{
'none'
}
userSelect=
{
'none'
}
{
...
(
isMaxSelected
||
disabled
{
...
(
isMaxSelected
||
disabled
...
@@ -394,9 +396,13 @@ const FileSelector = ({
...
@@ -394,9 +396,13 @@ const FileSelector = ({
cursor
:
'
pointer
',
cursor
:
'
pointer
',
_hover
:
{
_hover
:
{
bg
:
'
primary
.
50',
bg
:
'
primary
.
50',
borderColor
:
'
primary
.
600'
borderColor
:
isInvalid
?
'
red
.
500'
:
'
primary
.
600'
},
},
borderColor
:
isDragging
?
'
primary
.
600'
:
'
borderColor
.
high
',
borderColor
:
isInvalid
?
'
red
.
500'
:
isDragging
?
'
primary
.
600'
:
'
borderColor
.
high
',
onDragEnter
:
handleDragEnter
,
onDragEnter
:
handleDragEnter
,
onDragOver
:
(
e
)
=
>
e.preventDefault(),
onDragOver
:
(
e
)
=
>
e.preventDefault(),
onDragLeave: handleDragLeave,
onDragLeave: handleDragLeave,
...
@@ -436,17 +442,25 @@ const FileSelector = ({
...
@@ -436,17 +442,25 @@ const FileSelector = ({
/>
/>
<
Input
<
Input
isDisabled=
{
isMaxSelected
||
disabled
}
isDisabled=
{
isMaxSelected
||
disabled
}
isInvalid=
{
isInvalid
}
value=
{
urlInput
}
value=
{
urlInput
}
onChange=
{
(
e
)
=>
setUrlInput
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
setUrlInput
(
e
.
target
.
value
)
}
onBlur=
{
(
e
)
=>
handleAddUrl
(
e
.
target
.
value
)
}
onBlur=
{
(
e
)
=>
handleAddUrl
(
e
.
target
.
value
)
}
border=
{
'1.5px dashed'
}
border=
{
'1.5px dashed'
}
borderColor=
{
'myGray.250'
}
borderColor=
{
isInvalid
?
'red.500'
:
'myGray.250'
}
borderRadius=
{
'md'
}
borderRadius=
{
'md'
}
pl=
{
8
}
pl=
{
8
}
py=
{
1.5
}
py=
{
1.5
}
placeholder=
{
placeholder=
{
isMaxSelected
?
t
(
'file:reached_max_file_count'
)
:
t
(
'chat:click_to_add_url'
)
isMaxSelected
?
t
(
'file:reached_max_file_count'
)
:
t
(
'chat:click_to_add_url'
)
}
}
_hover=
{
{
borderColor
:
isInvalid
?
'red.500'
:
'myGray.300'
}
}
_focus=
{
{
borderColor
:
isInvalid
?
'red.500'
:
'primary.600'
,
boxShadow
:
isInvalid
?
'0 0 0 1px var(--chakra-colors-red-500)'
:
undefined
}
}
/>
/>
</
InputGroup
>
</
InputGroup
>
</
Box
>
</
Box
>
...
@@ -480,15 +494,29 @@ const FileSelector = ({
...
@@ -480,15 +494,29 @@ const FileSelector = ({
{
/* Status icon */
}
{
/* Status icon */
}
<>
<>
{
!!
file
?.
url
||
!!
file
?.
error
||
file
.
process
===
undefined
?
(
{
!!
file
?.
url
||
!!
file
?.
error
||
file
.
process
===
undefined
?
(
<
IconButton
<
HStack
spacing=
{
1
}
>
size=
{
'xsSquare'
}
{
/* View button - 查看文件 */
}
borderRadius=
{
'xs'
}
{
file
?.
url
&&
(
variant=
{
'transparentDanger'
}
<
IconButton
aria
-
label=
{
'Delete file'
}
size=
{
'xsSquare'
}
icon=
{
<
MyIcon
name=
{
'close'
}
w=
{
'1rem'
}
/>
}
variant=
{
'grayGhost'
}
onClick=
{
()
=>
handleDeleteFile
(
file
?.
id
)
}
aria
-
label=
{
'View file'
}
isDisabled=
{
disabled
}
icon=
{
<
MyIcon
name=
{
'common/viewLight'
}
w=
{
'1rem'
}
/>
}
/>
onClick=
{
()
=>
window
.
open
(
file
.
url
,
'_blank'
)
}
/>
)
}
{
/* Delete button - 只在未禁用时显示 */
}
{
!
disabled
&&
(
<
IconButton
size=
{
'xsSquare'
}
borderRadius=
{
'xs'
}
variant=
{
'transparentDanger'
}
aria
-
label=
{
'Delete file'
}
icon=
{
<
MyIcon
name=
{
'close'
}
w=
{
'1rem'
}
/>
}
onClick=
{
()
=>
handleDeleteFile
(
file
?.
id
)
}
/>
)
}
</
HStack
>
)
:
(
)
:
(
<
HStack
w=
{
'24px'
}
h=
{
'24px'
}
justifyContent=
{
'center'
}
>
<
HStack
w=
{
'24px'
}
h=
{
'24px'
}
justifyContent=
{
'center'
}
>
<
CircularProgress
<
CircularProgress
...
...
projects/app/src/components/core/app/formRender/index.tsx
View file @
6ea65f64
...
@@ -219,6 +219,7 @@ const InputRender = (props: InputRenderProps) => {
...
@@ -219,6 +219,7 @@ const InputRender = (props: InputRenderProps) => {
value=
{
files
}
value=
{
files
}
onChange=
{
(
e
)
=>
onChange
?.(
e
)
}
onChange=
{
(
e
)
=>
onChange
?.(
e
)
}
isDisabled=
{
isDisabled
}
isDisabled=
{
isDisabled
}
isInvalid=
{
isInvalid
}
maxFiles=
{
props
.
maxFiles
}
maxFiles=
{
props
.
maxFiles
}
canSelectFile=
{
props
.
canSelectFile
}
canSelectFile=
{
props
.
canSelectFile
}
canSelectImg=
{
props
.
canSelectImg
}
canSelectImg=
{
props
.
canSelectImg
}
...
...
projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts
View file @
6ea65f64
...
@@ -155,12 +155,27 @@ export const rewriteHistoriesByInteractiveResponse = ({
...
@@ -155,12 +155,27 @@ export const rewriteHistoriesByInteractiveResponse = ({
finalInteractive
.
type
===
'userInput'
||
finalInteractive
.
type
===
'userInput'
||
finalInteractive
.
type
===
'agentPlanAskUserForm'
finalInteractive
.
type
===
'agentPlanAskUserForm'
)
{
)
{
const
submittedData
:
Record
<
string
,
any
>
=
(()
=>
{
try
{
return
JSON
.
parse
(
interactiveVal
);
}
catch
(
error
)
{
return
{};
}
})();
// 更新 inputForm 中的 value
const
updatedInputForm
=
finalInteractive
.
params
.
inputForm
.
map
((
item
)
=>
({
...
item
,
value
:
submittedData
[
item
.
key
]
??
item
.
value
}));
return
{
return
{
...
val
,
...
val
,
interactive
:
{
interactive
:
{
...
finalInteractive
,
...
finalInteractive
,
params
:
{
params
:
{
...
finalInteractive
.
params
,
...
finalInteractive
.
params
,
inputForm
:
updatedInputForm
,
submitted
:
true
submitted
:
true
}
}
}
}
...
...
projects/app/src/components/core/chat/components/AIResponseBox.tsx
View file @
6ea65f64
...
@@ -245,34 +245,9 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
...
@@ -245,34 +245,9 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
}
}
});
});
if
(
typeof
window
!==
'undefined'
)
{
const
dataToSave
=
{
...
data
};
interactive
.
params
.
inputForm
?.
forEach
((
item
)
=>
{
// 这是干啥的?
if
(
item
.
type
===
'fileSelect'
&&
Array
.
isArray
(
dataToSave
[
item
.
key
])
&&
dataToSave
[
item
.
key
].
length
>
0
)
{
const
files
=
dataToSave
[
item
.
key
];
if
(
files
[
0
]?.
url
!==
undefined
)
{
dataToSave
[
item
.
key
]
=
files
.
map
((
file
:
any
)
=>
({
url
:
file
.
url
,
key
:
file
.
key
,
name
:
file
.
name
,
type
:
file
.
type
}))
.
filter
((
file
:
any
)
=>
file
.
url
);
}
}
});
sessionStorage
.
setItem
(
`interactiveForm_
${
chatItemDataId
}
`
,
JSON
.
stringify
(
dataToSave
));
}
onSendPrompt
(
JSON
.
stringify
(
finalData
));
onSendPrompt
(
JSON
.
stringify
(
finalData
));
},
},
[
chatItemDataId
,
interactive
.
params
.
inputForm
]
[
interactive
.
params
.
inputForm
]
);
);
return
(
return
(
...
...
projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx
View file @
6ea65f64
...
@@ -157,7 +157,7 @@ export const FormInputComponent = React.memo(function FormInputComponent({
...
@@ -157,7 +157,7 @@ export const FormInputComponent = React.memo(function FormInputComponent({
validate
:
(
value
)
=>
{
validate
:
(
value
)
=>
{
if
(
input
.
type
===
'password'
&&
input
.
minLength
)
{
if
(
input
.
type
===
'password'
&&
input
.
minLength
)
{
if
(
!
value
||
typeof
value
!==
'object'
||
!
value
.
value
)
{
if
(
!
value
||
typeof
value
!==
'object'
||
!
value
.
value
)
{
return
false
;
return
t
(
'common:required'
)
;
}
}
if
(
value
.
value
.
length
<
input
.
minLength
)
{
if
(
value
.
value
.
length
<
input
.
minLength
)
{
return
t
(
'common:min_length'
,
{
minLenth
:
input
.
minLength
});
return
t
(
'common:min_length'
,
{
minLenth
:
input
.
minLength
});
...
@@ -188,7 +188,7 @@ export const FormInputComponent = React.memo(function FormInputComponent({
...
@@ -188,7 +188,7 @@ export const FormInputComponent = React.memo(function FormInputComponent({
isInvalid=
{
!!
error
}
isInvalid=
{
!!
error
}
isRichText=
{
false
}
isRichText=
{
false
}
/>
/>
{
error
&&
<
FormErrorMessage
>
{
error
.
message
}
</
FormErrorMessage
>
}
{
error
&&
error
.
message
&&
<
FormErrorMessage
>
{
error
.
message
}
</
FormErrorMessage
>
}
</
FormControl
>
</
FormControl
>
);
);
}
}
}
}
...
...
projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx
View file @
6ea65f64
...
@@ -525,8 +525,9 @@ const NodeIntro = React.memo(function NodeIntro({
...
@@ -525,8 +525,9 @@ const NodeIntro = React.memo(function NodeIntro({
intro
?:
string
;
intro
?:
string
;
})
{
})
{
const
{
t
}
=
useTranslation
();
const
{
t
}
=
useTranslation
();
const
nodeIsTool
=
useContextSelector
(
WorkflowUtilsContext
,
(
ctx
)
=>
const
nodeIsTool
=
useContextSelector
(
ctx
.
splitToolInputs
([],
nodeId
)
WorkflowUtilsContext
,
(
ctx
)
=>
ctx
.
splitToolInputs
([],
nodeId
)?.
isTool
);
);
const
onChangeNode
=
useContextSelector
(
WorkflowActionsContext
,
(
v
)
=>
v
.
onChangeNode
);
const
onChangeNode
=
useContextSelector
(
WorkflowActionsContext
,
(
v
)
=>
v
.
onChangeNode
);
...
@@ -544,32 +545,32 @@ const NodeIntro = React.memo(function NodeIntro({
...
@@ -544,32 +545,32 @@ const NodeIntro = React.memo(function NodeIntro({
<
Box
fontSize=
{
'sm'
}
color=
{
'myGray.500'
}
flex=
{
'1 0 0'
}
>
<
Box
fontSize=
{
'sm'
}
color=
{
'myGray.500'
}
flex=
{
'1 0 0'
}
>
{
t
(
intro
as
any
)
||
t
(
'app:node_not_intro'
)
}
{
t
(
intro
as
any
)
||
t
(
'app:node_not_intro'
)
}
</
Box
>
</
Box
>
{
nodeIsTool
&&
(
<
Flex
<
Flex
className=
"node-hover-controller"
p=
{
'7px
'
}
visibility=
{
nodeIsTool
?
'visible'
:
'hidden
'
}
rounded=
{
'sm
'
}
p=
{
'7px
'
}
alignItems=
{
'center
'
}
rounded=
{
'sm
'
}
_hover=
{
{
alignItems=
{
'center'
}
bg
:
'myGray.100'
_hover=
{
{
}
}
bg
:
'myGray.100'
cursor=
{
'pointer'
}
}
}
onClick=
{
()
=>
{
cursor=
{
'pointer'
}
onOpenIntroModal
(
{
onClick=
{
()
=>
{
defaultVal
:
intro
,
onOpenIntroModal
({
onSuccess
(
e
)
{
defaultVal
:
intro
,
onChangeNode
(
{
onSuccess
(
e
)
{
nodeId
,
onChangeNode
({
type
:
'attr'
,
nodeId
,
key
:
'intro
'
,
type
:
'attr
'
,
value
:
e
key
:
'intro'
,
});
value
:
e
}
}
);
}
);
}
}
}
}
);
>
}
}
<
MyIcon
name=
{
'edit'
}
w=
{
'18px'
}
/
>
>
<
/
Flex
>
<
MyIcon
name=
{
'edit'
}
w=
{
'18px'
}
/
>
)
}
</
Flex
>
</
Flex
>
</
Flex
>
<
EditIntroModal
maxLength=
{
500
}
/>
<
EditIntroModal
maxLength=
{
500
}
/>
</>
</>
...
...
test/cases/global/core/app/jsonschema.test.ts
View file @
6ea65f64
...
@@ -253,6 +253,124 @@ describe('getNodeInputTypeFromSchemaInputType', () => {
...
@@ -253,6 +253,124 @@ describe('getNodeInputTypeFromSchemaInputType', () => {
});
});
expect
(
result
).
toBe
(
WorkflowIOValueTypeEnum
.
arrayAny
);
expect
(
result
).
toBe
(
WorkflowIOValueTypeEnum
.
arrayAny
);
});
});
it
(
'should return any when type is undefined (for anyOf/oneOf)'
,
()
=>
{
const
result
=
getNodeInputTypeFromSchemaInputType
({
type
:
undefined
,
arrayItems
:
undefined
});
expect
(
result
).
toBe
(
WorkflowIOValueTypeEnum
.
any
);
});
});
describe
(
'jsonSchema2NodeInput with anyOf/oneOf (union types)'
,
()
=>
{
it
(
'should handle Optional[str] with anyOf as any type'
,
()
=>
{
const
jsonSchema
:
JSONSchemaInputType
=
{
type
:
'object'
,
properties
:
{
optional_field
:
{
anyOf
:
[{
type
:
'string'
},
{
type
:
'null'
}],
description
:
'An optional string field'
}
},
required
:
[]
};
const
result
=
jsonSchema2NodeInput
({
jsonSchema
,
schemaType
:
'mcp'
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
key
).
toBe
(
'optional_field'
);
expect
(
result
[
0
].
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
any
);
expect
(
result
[
0
].
required
).
toBe
(
false
);
expect
(
result
[
0
].
description
).
toBe
(
'An optional string field'
);
});
it
(
'should handle oneOf with null as any type'
,
()
=>
{
const
jsonSchema
:
JSONSchemaInputType
=
{
type
:
'object'
,
properties
:
{
optional_number
:
{
oneOf
:
[{
type
:
'number'
},
{
type
:
'null'
}]
}
}
};
const
result
=
jsonSchema2NodeInput
({
jsonSchema
,
schemaType
:
'mcp'
});
expect
(
result
[
0
].
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
any
);
});
it
(
'should handle mixed schema with anyOf and regular types'
,
()
=>
{
const
jsonSchema
:
JSONSchemaInputType
=
{
type
:
'object'
,
properties
:
{
required_field
:
{
type
:
'string'
,
description
:
'A required string'
},
optional_field
:
{
anyOf
:
[{
type
:
'string'
},
{
type
:
'null'
}],
description
:
'An optional string'
},
number_field
:
{
type
:
'number'
}
},
required
:
[
'required_field'
]
};
const
result
=
jsonSchema2NodeInput
({
jsonSchema
,
schemaType
:
'mcp'
});
expect
(
result
).
toHaveLength
(
3
);
const
requiredField
=
result
.
find
((
i
)
=>
i
.
key
===
'required_field'
);
expect
(
requiredField
?.
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
string
);
expect
(
requiredField
?.
required
).
toBe
(
true
);
const
optionalField
=
result
.
find
((
i
)
=>
i
.
key
===
'optional_field'
);
expect
(
optionalField
?.
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
any
);
expect
(
optionalField
?.
required
).
toBe
(
false
);
const
numberField
=
result
.
find
((
i
)
=>
i
.
key
===
'number_field'
);
expect
(
numberField
?.
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
number
);
});
it
(
'should handle weather API real-world example'
,
()
=>
{
const
jsonSchema
:
JSONSchemaInputType
=
{
type
:
'object'
,
properties
:
{
location
:
{
type
:
'string'
,
description
:
'地点名称'
},
date
:
{
anyOf
:
[{
type
:
'string'
},
{
type
:
'null'
}],
description
:
'日期(可选)'
},
forecast_type
:
{
type
:
'string'
,
enum
:
[
'daily'
,
'hourly'
,
'weekly'
]
}
},
required
:
[
'location'
]
};
const
result
=
jsonSchema2NodeInput
({
jsonSchema
,
schemaType
:
'mcp'
});
expect
(
result
).
toHaveLength
(
3
);
const
location
=
result
.
find
((
i
)
=>
i
.
key
===
'location'
);
expect
(
location
?.
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
string
);
expect
(
location
?.
required
).
toBe
(
true
);
const
date
=
result
.
find
((
i
)
=>
i
.
key
===
'date'
);
expect
(
date
?.
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
any
);
expect
(
date
?.
required
).
toBe
(
false
);
const
forecastType
=
result
.
find
((
i
)
=>
i
.
key
===
'forecast_type'
);
expect
(
forecastType
?.
valueType
).
toBe
(
WorkflowIOValueTypeEnum
.
string
);
expect
(
forecastType
?.
list
).
toHaveLength
(
3
);
});
});
});
describe
(
'jsonSchema2NodeOutput'
,
()
=>
{
describe
(
'jsonSchema2NodeOutput'
,
()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment