Commit ffa1037a by Finley Ge Committed by GitHub

refactor: align system tool JSON schema codec (#7116)

* refactor(core): optimize system tool input/output schema management

- Use `JSONSchema` instead of `InputConfig` for system tool I/O
  definitions
- Centralize JSON Schema to Node I/O conversion utilities
- Cleanup redundant type definitions and schema properties across
  packages

* refactor(app): optimize SecretInputModal UI and logic

- Migrate to updated MyModal component and refactor layout
- Update input styling and placeholder logic
- Add missing input controller for 'input' type fields

* refactor(global): improve workflow JSON schema mapping and tool catalog

- Update `getJsonSchemaPropertyFromValueType` to handle complex types
  like `arrayObject` and `chatHistory` properly instead of falling back
  to default types.
- Refactor `getEnumValuesFromNodeInput` to correctly collect enum values
  from different input configurations.
- Update `useToolCatalog` to use `nodeInputs2JsonSchema` for consistent
  schema generation across tools and nodes.
- Add comprehensive test cases to verify JSON schema conversion for
  various value types and enum sources.

* perf: get tool client code

* chore: avoid schema loading for tool display metadata

* perf: tool throw error

---------

Co-authored-by: archer <545436317@qq.com>
parent cc59ee8b
...@@ -104,6 +104,7 @@ rg -n "关键函数|关键类型|关键字段" <相关目录> ...@@ -104,6 +104,7 @@ rg -n "关键函数|关键类型|关键字段" <相关目录>
对比相邻实现,而不是只按个人偏好判断: 对比相邻实现,而不是只按个人偏好判断:
- API route、service、schema、hook、组件、i18n、错误处理、日志、权限判断是否沿用附近模式。 - API route、service、schema、hook、组件、i18n、错误处理、日志、权限判断是否沿用附近模式。
- i18n 调用必须使用可被清理脚本静态识别的 key:不要写 ``t(`ns:key_${value}`)```t(prefix + value)``t(variableKey)``t(condition ? 'ns:a' : 'ns:b')` 这类动态/表达式 key;有限枚举应拆成显式分支或静态映射,并在最终调用处保留 `t('ns:literal_key')` 形式。改动后要检查对应语言包 key 是否齐全,避免清理脚本误删或运行时裸显 key。
- 前端交互、Chakra UI 组件组织、状态命名、弹窗/表单/列表布局是否与同模块一致。 - 前端交互、Chakra UI 组件组织、状态命名、弹窗/表单/列表布局是否与同模块一致。
- 后端命名、目录分层、模型访问、事务/队列/缓存写法是否与同类代码一致。 - 后端命名、目录分层、模型访问、事务/队列/缓存写法是否与同类代码一致。
- 注释是否解释设计原因,尤其是导出函数、核心业务函数、复杂 helper;避免无意义逐行注释。 - 注释是否解释设计原因,尤其是导出函数、核心业务函数、复杂 helper;避免无意义逐行注释。
......
...@@ -107,6 +107,7 @@ git branch -D pr/<number> ...@@ -107,6 +107,7 @@ git branch -D pr/<number>
对照 FastGPT 各项规范逐一检查,完成以下检查清单: 对照 FastGPT 各项规范逐一检查,完成以下检查清单:
- [] **i18n 静态 key 规范**:检查新增/修改的 `t(...)` 调用是否能被 i18n 清理脚本静态识别。禁止 ``t(`ns:key_${value}`)```t(prefix + value)``t(variableKey)``t(condition ? 'ns:a' : 'ns:b')` 等动态或表达式 key;有限枚举必须拆成显式分支或静态映射,并让最终调用保持 `t('ns:literal_key')` 形式。同时确认所有被引用 key 已补齐到对应语言包,避免被误清理或运行时裸显 key。
- [] [API 路由开发规范](../../design/api/index.md) - [] [API 路由开发规范](../../design/api/index.md)
- [] [前端组件规范](./style/front.md) - [] [前端组件规范](./style/front.md)
- [] [数据库规范](./style/db.md) - [] [数据库规范](./style/db.md)
......
...@@ -32,6 +32,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initSandboxArchive' \ ...@@ -32,6 +32,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initSandboxArchive' \
## 🐛 修复 ## 🐛 修复
1. 修复 S3 私有对象 key 未绑定已鉴权资源时可能导致的跨资源文件访问风险。 1. 修复 S3 私有对象 key 未绑定已鉴权资源时可能导致的跨资源文件访问风险。
2. 工作流工具,array 和 object 类型,工具调用参数 schema 异常。
## 代码优化 ## 代码优化
......
...@@ -285,7 +285,7 @@ ...@@ -285,7 +285,7 @@
"content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-10T19:02:59+08:00", "content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-10T19:02:59+08:00",
"content/self-host/upgrading/4-15/41504.mdx": "2026-06-15T23:34:43+08:00", "content/self-host/upgrading/4-15/41504.mdx": "2026-06-15T23:34:43+08:00",
"content/self-host/upgrading/4-15/41505.en.mdx": "2026-06-12T20:47:04+08:00", "content/self-host/upgrading/4-15/41505.en.mdx": "2026-06-12T20:47:04+08:00",
"content/self-host/upgrading/4-15/41505.mdx": "2026-06-16T17:39:42+08:00", "content/self-host/upgrading/4-15/41505.mdx": "2026-06-17T17:02:34+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00",
......
...@@ -44,8 +44,6 @@ export type AdminSystemToolChildDetailType = z.infer<typeof AdminSystemToolChild ...@@ -44,8 +44,6 @@ export type AdminSystemToolChildDetailType = z.infer<typeof AdminSystemToolChild
/** 系统工具的详细信息 */ /** 系统工具的详细信息 */
export const AdminSystemToolDetailSchema = z.object({ export const AdminSystemToolDetailSchema = z.object({
...SystemToolDetailSchema.omit({ ...SystemToolDetailSchema.omit({
inputs: true,
outputs: true,
isLatestVersion: true, isLatestVersion: true,
children: true children: true
}).shape, }).shape,
......
import z from 'zod'; import z from 'zod';
import { PluginStatusSchema } from '../../../../plugin/type'; import { PluginStatusSchema } from '../../../../plugin/type';
import { UserTagsSchema } from '../../../../../support/user/type'; import { UserTagsSchema } from '../../../../../support/user/type';
import {
FlowNodeInputItemTypeSchema,
FlowNodeOutputItemTypeSchema,
InputConfigTypeSchema
} from '../../../../workflow/type/io';
import { PluginPermissionEnumSchema } from '../../../../../sdk/fastgpt-plugin'; import { PluginPermissionEnumSchema } from '../../../../../sdk/fastgpt-plugin';
import { SystemToolSystemSecretStatusEnum } from '../constants'; import { SystemToolSystemSecretStatusEnum } from '../constants';
import { JSONSchemaInputTypeSchema, JSONSchemaOutputTypeSchema } from '../../../jsonschema';
// 系统工具最基础最通用的类型 // 系统工具最基础最通用的类型
export const SystemToolBaseSchema = z.object({ export const SystemToolBaseSchema = z.object({
...@@ -61,7 +57,6 @@ export const SystemToolListItemSchema = z.object({ ...@@ -61,7 +57,6 @@ export const SystemToolListItemSchema = z.object({
.enum(SystemToolSystemSecretStatusEnum) .enum(SystemToolSystemSecretStatusEnum)
.default(SystemToolSystemSecretStatusEnum.none) .default(SystemToolSystemSecretStatusEnum.none)
.meta({ description: '系统密钥配置状态' }), .meta({ description: '系统密钥配置状态' }),
secrets: z.array(InputConfigTypeSchema).optional(),
// 用户筛选 // 用户筛选
hideTags: z.array(UserTagsSchema).optional(), hideTags: z.array(UserTagsSchema).optional(),
...@@ -78,22 +73,20 @@ export const SystemToolChildDetailSchema = z.object({ ...@@ -78,22 +73,20 @@ export const SystemToolChildDetailSchema = z.object({
icon: z.string().optional(), icon: z.string().optional(),
currentCost: z.number().meta({ description: '当前使用的费用' }), currentCost: z.number().meta({ description: '当前使用的费用' }),
systemKeyCost: z.number().meta({ description: '系统密钥的费用' }), systemKeyCost: z.number().meta({ description: '系统密钥的费用' }),
inputs: z.array(FlowNodeInputItemTypeSchema), inputSchema: JSONSchemaInputTypeSchema.optional(),
outputs: z.array(FlowNodeOutputItemTypeSchema) outputSchema: JSONSchemaOutputTypeSchema.optional()
}); });
export type SystemToolChildDetailType = z.infer<typeof SystemToolChildDetailSchema>; export type SystemToolChildDetailType = z.infer<typeof SystemToolChildDetailSchema>;
/** 系统工具的详细信息 /** 系统工具的详细信息。Repo 只返回 JSON Schema,节点 IO 由 presenter/runtime 按需转换。 */
* TODO: input, output, secret 这些类型其实并不合理,应当是更干净的类型, 后续再迁移
*/
export const SystemToolDetailSchema = z.object({ export const SystemToolDetailSchema = z.object({
...SystemToolListItemSchema.shape, ...SystemToolListItemSchema.shape,
children: z.array(SystemToolChildDetailSchema).optional(), children: z.array(SystemToolChildDetailSchema).optional(),
inputs: z.array(FlowNodeInputItemTypeSchema).optional(), inputSchema: JSONSchemaInputTypeSchema.optional(),
outputs: z.array(FlowNodeOutputItemTypeSchema).optional(), outputSchema: JSONSchemaOutputTypeSchema.optional(),
secrets: z.array(InputConfigTypeSchema).optional(), secretSchema: JSONSchemaInputTypeSchema.optional(),
secretsVal: z.record(z.string(), z.any()).nullish(), secretsVal: z.record(z.string(), z.any()).nullish(),
isLatestVersion: z.boolean().optional(), isLatestVersion: z.boolean().optional(),
associatedPluginId: z.string().optional(), associatedPluginId: z.string().optional(),
......
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
SystemToolListItemSchema SystemToolListItemSchema
} from '../../../../../core/app/tool/systemTool/type'; } from '../../../../../core/app/tool/systemTool/type';
import { SystemToolVersionSchema } from '../../../../../core/app/tool/systemTool/type/base'; import { SystemToolVersionSchema } from '../../../../../core/app/tool/systemTool/type/base';
import { OpenAPIFlowNodeOutputItemTypeSchema } from '../../../workflow/node';
/* ============================================================================ /* ============================================================================
* API: 获取团队插件列表 * API: 获取团队插件列表
...@@ -65,19 +64,7 @@ export const TeamToolDetailSchema = z.object({ ...@@ -65,19 +64,7 @@ export const TeamToolDetailSchema = z.object({
}); });
export type GetTeamToolDetailResponseType = z.infer<typeof TeamToolDetailSchema>; export type GetTeamToolDetailResponseType = z.infer<typeof TeamToolDetailSchema>;
const OpenAPISystemToolChildDetailSchema = SystemToolChildDetailSchema.omit({ export const OpenAPITeamToolDetailSchema = TeamToolDetailSchema;
outputs: true
}).extend({
outputs: z.array(OpenAPIFlowNodeOutputItemTypeSchema)
});
export const OpenAPITeamToolDetailSchema = TeamToolDetailSchema.omit({
outputs: true,
children: true
}).extend({
outputs: z.array(OpenAPIFlowNodeOutputItemTypeSchema).optional(),
children: z.array(OpenAPISystemToolChildDetailSchema).optional()
});
/* ============================================================================ /* ============================================================================
* API: 获取团队工具版本列表 * API: 获取团队工具版本列表
......
...@@ -6,13 +6,20 @@ import type { ...@@ -6,13 +6,20 @@ import type {
import { import {
jsonSchema2NodeInput, jsonSchema2NodeInput,
jsonSchema2NodeOutput, jsonSchema2NodeOutput,
jsonSchema2SecretInput,
inputConfigs2JsonSchema,
getNodeInputTypeFromSchemaInputType, getNodeInputTypeFromSchemaInputType,
getSchemaValueType, getSchemaValueType,
nodeInputs2JsonSchema,
nodeOutputs2JsonSchema,
str2OpenApiSchema str2OpenApiSchema
} from '@fastgpt/global/core/app/jsonschema'; } from '@fastgpt/global/core/app/jsonschema';
import { bundleOpenAPISchema } from '@fastgpt/global/common/string/swagger'; import { bundleOpenAPISchema } from '@fastgpt/global/common/string/swagger';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputItemTypeSchema } from '@fastgpt/global/core/workflow/type/io'; import {
FlowNodeInputItemTypeSchema,
InputConfigInputTypeEnum
} from '@fastgpt/global/core/workflow/type/io';
describe('jsonSchema2NodeInput', () => { describe('jsonSchema2NodeInput', () => {
it('should return correct node input for http schema', () => { it('should return correct node input for http schema', () => {
...@@ -566,6 +573,338 @@ describe('jsonSchema2NodeOutput', () => { ...@@ -566,6 +573,338 @@ describe('jsonSchema2NodeOutput', () => {
}); });
}); });
describe('nodeInputs2JsonSchema', () => {
it('should convert node inputs to json schema properties', () => {
const result = nodeInputs2JsonSchema({
inputs: [
{
key: 'query',
label: 'Query',
valueType: WorkflowIOValueTypeEnum.string,
toolDescription: 'Search query',
description: 'UI description',
required: true,
renderTypeList: ['input', 'reference']
},
{
key: 'platform',
label: 'Platform',
valueType: WorkflowIOValueTypeEnum.arrayString,
toolDescription: 'Select platforms',
required: false,
renderTypeList: ['multipleSelect', 'reference'],
list: [
{ label: 'Zhihu', value: 'zhihu' },
{ label: 'Weibo', value: 'weibo' }
]
},
{
key: 'legacy',
label: 'Legacy',
valueType: WorkflowIOValueTypeEnum.string,
toolDescription: 'Legacy enum',
required: false,
renderTypeList: ['select', 'reference'],
enum: 'a\nb'
}
]
});
expect(result).toEqual({
type: 'object',
properties: {
query: {
type: 'string',
title: 'Query',
description: 'Search query',
toolDescription: 'Search query'
},
platform: {
type: 'array',
items: {
type: 'string',
enum: ['zhihu', 'weibo']
},
title: 'Platform',
description: 'Select platforms',
toolDescription: 'Select platforms'
},
legacy: {
type: 'string',
enum: ['a', 'b'],
title: 'Legacy',
description: 'Legacy enum',
toolDescription: 'Legacy enum'
}
},
required: ['query']
});
});
it('should convert select options from list and enums to json schema enum', () => {
const result = nodeInputs2JsonSchema({
inputs: [
{
key: 'singleSelection',
label: 'singleSelection',
valueType: WorkflowIOValueTypeEnum.string,
toolDescription: 'Select one option',
renderTypeList: ['select', 'reference'],
list: [
{ label: 'Option A', value: 'A' },
{ label: 'Option B', value: 'B' }
]
},
{
key: 'fallbackSelection',
label: 'fallbackSelection',
valueType: WorkflowIOValueTypeEnum.string,
toolDescription: 'Select one fallback option',
renderTypeList: ['select', 'reference'],
list: [],
enums: [
{ label: 'Option C', value: 'C' },
{ label: 'Option D', value: 'D' }
]
}
]
});
expect(result.properties).toMatchObject({
singleSelection: {
type: 'string',
enum: ['A', 'B']
},
fallbackSelection: {
type: 'string',
enum: ['C', 'D']
}
});
});
it('should keep non-string workflow value types from falling back to string schema', () => {
const result = nodeInputs2JsonSchema({
inputs: [
{
key: 'items',
label: 'Items',
valueType: WorkflowIOValueTypeEnum.arrayObject,
renderTypeList: ['JSONEditor', 'reference']
},
{
key: 'history',
label: 'History',
valueType: WorkflowIOValueTypeEnum.chatHistory,
renderTypeList: ['JSONEditor', 'reference']
},
{
key: 'quote',
label: 'Quote',
valueType: WorkflowIOValueTypeEnum.datasetQuote,
renderTypeList: ['JSONEditor', 'reference']
},
{
key: 'dynamic',
label: 'Dynamic',
valueType: WorkflowIOValueTypeEnum.dynamic,
renderTypeList: ['JSONEditor', 'reference']
},
{
key: 'dataset',
label: 'Dataset',
valueType: WorkflowIOValueTypeEnum.selectDataset,
renderTypeList: ['JSONEditor', 'reference']
},
{
key: 'app',
label: 'App',
valueType: WorkflowIOValueTypeEnum.selectApp,
renderTypeList: ['JSONEditor', 'reference']
}
]
});
expect(result.properties).toEqual({
items: {
type: 'array',
items: { type: 'object' },
title: 'Items',
description: ''
},
history: {
type: 'array',
items: { type: 'object' },
title: 'History',
description: ''
},
quote: {
type: 'array',
items: { type: 'object' },
title: 'Quote',
description: ''
},
dynamic: {
title: 'Dynamic',
description: ''
},
dataset: {
title: 'Dataset',
description: ''
},
app: {
title: 'App',
description: ''
}
});
});
});
describe('nodeOutputs2JsonSchema', () => {
it('should convert node outputs to json schema properties', () => {
const result = nodeOutputs2JsonSchema({
outputs: [
{
id: 'result',
key: 'result',
label: 'Result',
type: 'static',
valueType: WorkflowIOValueTypeEnum.object,
description: 'Result object',
required: true
}
]
});
expect(result).toEqual({
type: 'object',
properties: {
result: {
type: 'object',
title: 'Result',
description: 'Result object'
}
},
required: ['result']
});
});
it('should convert arrayObject node outputs to array object schema', () => {
const result = nodeOutputs2JsonSchema({
outputs: [
{
id: 'items',
key: 'items',
label: 'Items',
type: 'static',
valueType: WorkflowIOValueTypeEnum.arrayObject,
description: 'Object list'
}
]
});
expect(result.properties?.items).toEqual({
type: 'array',
items: { type: 'object' },
title: 'Items',
description: 'Object list'
});
});
});
describe('inputConfigs2JsonSchema', () => {
it('should convert input config to secret schema', () => {
const result = inputConfigs2JsonSchema({
inputConfigs: [
{
key: 'apiKey',
label: 'API Key',
inputType: InputConfigInputTypeEnum.secret,
description: 'Secret key',
required: true
},
{
key: 'region',
label: 'Region',
inputType: InputConfigInputTypeEnum.select,
list: [
{ label: 'US', value: 'us' },
{ label: 'EU', value: 'eu' }
]
}
]
});
expect(result).toEqual({
type: 'object',
properties: {
apiKey: {
type: 'string',
isSecret: true,
title: 'API Key',
description: 'Secret key'
},
region: {
type: 'string',
enum: ['us', 'eu'],
title: 'Region',
description: ''
}
},
required: ['apiKey']
});
});
});
describe('jsonSchema2SecretInput', () => {
it('should convert scalar and array enums to select options', () => {
const result = jsonSchema2SecretInput({
jsonSchema: {
type: 'object',
properties: {
region: {
type: 'string',
title: 'Region',
enum: ['us', 'eu']
},
scopes: {
type: 'array',
title: 'Scopes',
items: {
type: 'string',
enum: ['read', 'write']
}
}
}
}
});
expect(result).toEqual([
{
inputType: InputConfigInputTypeEnum.select,
key: 'region',
label: 'Region',
description: undefined,
required: undefined,
list: [
{ label: 'us', value: 'us' },
{ label: 'eu', value: 'eu' }
]
},
{
inputType: InputConfigInputTypeEnum.select,
key: 'scopes',
label: 'Scopes',
description: undefined,
required: undefined,
list: [
{ label: 'read', value: 'read' },
{ label: 'write', value: 'write' }
]
}
]);
});
});
describe('getSchemaValueType', () => { describe('getSchemaValueType', () => {
it('should return number for integer type', () => { it('should return number for integer type', () => {
const result = getSchemaValueType({ type: 'integer' }); const result = getSchemaValueType({ type: 'integer' });
......
import type { localeType } from '@fastgpt/global/common/i18n/type';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { SystemToolRepo } from './systemTool/systemTool.repo';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { Output_Template_Error_Message } from '@fastgpt/global/core/workflow/template/output';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
/**
* 获得工具的 Template 类型供工作流渲染
*/
export async function getToolPreviewNode({
pluginId,
versionId,
getLatestVersion,
lang = 'en',
source: toolSource = 'system'
}: {
pluginId: string;
versionId?: string;
getLatestVersion?: boolean;
lang?: localeType;
source?: string;
}): Promise<FlowNodeTemplateType> {
const systemToolRepo = SystemToolRepo.getInstance();
const toolDetail = await systemToolRepo.getSystemToolDetail({
pluginId,
version: versionId || undefined,
lang,
source: toolSource
});
const shouldReturnVersion = versionId ? true : versionId === undefined && getLatestVersion;
const inputs = [
...(toolDetail.secrets?.length
? [
{
key: NodeInputKeyEnum.systemInputConfig,
label: '',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
inputList: toolDetail.secrets
}
]
: []),
...(toolDetail.inputs ?? [])
];
const isWorkflowTool = !!toolDetail.associatedPluginId;
return {
id: getNanoid(),
pluginId: pluginId,
flowNodeType: isWorkflowTool
? FlowNodeTypeEnum.pluginModule
: toolDetail.isToolSet
? FlowNodeTypeEnum.toolSet
: FlowNodeTypeEnum.tool,
avatar: toolDetail.avatar,
name: toolDetail.name,
intro: toolDetail.intro,
toolDescription: toolDetail.toolDescription,
courseUrl: toolDetail.courseUrl,
readmeUrl: toolDetail.readmeUrl,
userGuide: toolDetail.userGuide ?? undefined,
showStatus: true,
isTool: true,
catchError: false,
version: shouldReturnVersion ? toolDetail.version : '',
versionLabel: shouldReturnVersion ? (toolDetail.versionLabel ?? toolDetail.version) : undefined,
isLatestVersion: toolDetail.isLatestVersion,
showSourceHandle: true,
showTargetHandle: true,
currentCost: toolDetail.currentCost,
systemKeyCost: toolDetail.systemKeyCost,
hasTokenFee: toolDetail.hasTokenFee,
hasSystemSecret: toolDetail.hasSystemSecret,
isFolder: !isWorkflowTool && toolDetail.isToolSet,
status: toolDetail.status,
inputs,
outputs: toolDetail.outputs
? toolDetail.outputs.some((item) => item.type === FlowNodeOutputTypeEnum.error)
? toolDetail.outputs
: [...toolDetail.outputs, Output_Template_Error_Message]
: [],
...(isWorkflowTool
? {}
: {
toolConfig: {
...(toolDetail.isToolSet
? {
systemToolSet: {
toolId: pluginId,
toolList:
toolDetail.children?.map((child) => ({
description: child.description ?? '',
name: child.name,
toolId: child.id
})) ?? []
}
}
: {
systemTool: {
toolId: pluginId
}
})
}
})
} satisfies FlowNodeTemplateType;
}
...@@ -5,7 +5,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; ...@@ -5,7 +5,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils'; import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { getChildAppPreviewNode } from './tool/controller'; import { getClientToolPreviewNode } from './tool/utils/client';
import { authAppByTmbId } from '../../support/permission/app/auth'; import { authAppByTmbId } from '../../support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
...@@ -46,7 +46,7 @@ export async function rewriteAppWorkflowToDetail({ ...@@ -46,7 +46,7 @@ export async function rewriteAppWorkflowToDetail({
try { try {
const [preview] = await Promise.all([ const [preview] = await Promise.all([
getChildAppPreviewNode({ getClientToolPreviewNode({
appId: id, appId: id,
versionId, versionId,
lang lang
......
...@@ -18,12 +18,14 @@ import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/uti ...@@ -18,12 +18,14 @@ import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/uti
import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/tool/httpTool/utils'; import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/tool/httpTool/utils';
import type { ChatCompletionTool } from '@fastgpt/global/core/ai/llm/type'; import type { ChatCompletionTool } from '@fastgpt/global/core/ai/llm/type';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema';
import { import {
NodeInputKeyEnum, jsonSchema2NodeInput,
toolValueTypeList, jsonSchema2NodeOutput,
valueTypeJsonSchemaMap jsonSchema2SecretInput,
} from '@fastgpt/global/core/workflow/constants'; nodeInputs2JsonSchema,
type JSONSchemaInputType
} from '@fastgpt/global/core/app/jsonschema';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { import type {
McpToolConfigType, McpToolConfigType,
McpToolDataType McpToolDataType
...@@ -58,7 +60,7 @@ type AgentRuntimeNode = RuntimeNodeItemType & { ...@@ -58,7 +60,7 @@ type AgentRuntimeNode = RuntimeNodeItemType & {
/** /**
* 将 Agent 选择的工具配置转换成 LLM function calling 与 runtime 执行共用的工具描述。 * 将 Agent 选择的工具配置转换成 LLM function calling 与 runtime 执行共用的工具描述。
* *
* 这里刻意不调用面向前端预览的 getChildAppPreviewNode:Agent runtime 只关心鉴权后的执行 * 这里刻意不调用面向前端预览的 getClientToolPreviewNode:Agent runtime 只关心鉴权后的执行
* 节点、toolConfig 和 JSON Schema。App 类工具统一读取当前发布版本;MCP/HTTP 工具集只使用 * 节点、toolConfig 和 JSON Schema。App 类工具统一读取当前发布版本;MCP/HTTP 工具集只使用
* 当前版本节点里保存的 toolList,不额外兼容旧版 MCP 数据源。 * 当前版本节点里保存的 toolList,不额外兼容旧版 MCP 数据源。
*/ */
...@@ -117,19 +119,25 @@ export const getAgentRuntimeTools = async ({ ...@@ -117,19 +119,25 @@ export const getAgentRuntimeTools = async ({
source: source === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : 'system' source: source === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : 'system'
}); });
const isWorkflowTool = !!toolDetail.associatedPluginId; const isWorkflowTool = !!toolDetail.associatedPluginId;
const secrets = jsonSchema2SecretInput({ jsonSchema: toolDetail.secretSchema });
const schemaInputs = jsonSchema2NodeInput({
jsonSchema: toolDetail.inputSchema,
schemaType: 'systemTool'
});
const schemaOutputs = jsonSchema2NodeOutput({ jsonSchema: toolDetail.outputSchema });
// secrets 是运行时私密配置,只放进隐藏 input,不能暴露成模型可填写参数。 // secrets 是运行时私密配置,只放进隐藏 input,不能暴露成模型可填写参数。
const inputs = [ const inputs = [
...(toolDetail.secrets?.length ...(secrets?.length
? [ ? [
{ {
key: NodeInputKeyEnum.systemInputConfig, key: NodeInputKeyEnum.systemInputConfig,
label: '', label: '',
renderTypeList: [FlowNodeInputTypeEnum.hidden], renderTypeList: [FlowNodeInputTypeEnum.hidden],
inputList: toolDetail.secrets inputList: secrets
} satisfies FlowNodeInputItemType } satisfies FlowNodeInputItemType
] ]
: []), : []),
...(toolDetail.inputs ?? []) ...schemaInputs
]; ];
return { return {
...@@ -146,7 +154,8 @@ export const getAgentRuntimeTools = async ({ ...@@ -146,7 +154,8 @@ export const getAgentRuntimeTools = async ({
toolDescription: toolDetail.toolDescription, toolDescription: toolDetail.toolDescription,
version: '', version: '',
inputs, inputs,
outputs: appendErrorOutput(toolDetail.outputs ?? []), outputs: appendErrorOutput(schemaOutputs),
jsonSchema: toolDetail.inputSchema,
currentCost: toolDetail.currentCost, currentCost: toolDetail.currentCost,
hasSystemSecret: toolDetail.hasSystemSecret, hasSystemSecret: toolDetail.hasSystemSecret,
hasTokenFee: toolDetail.hasTokenFee, hasTokenFee: toolDetail.hasTokenFee,
...@@ -416,49 +425,12 @@ export const getAgentRuntimeTools = async ({ ...@@ -416,49 +425,12 @@ export const getAgentRuntimeTools = async ({
}; };
} }
const properties: Record<string, any> = {};
toolParams.forEach((param) => {
const jsonSchema = param.valueType
? valueTypeJsonSchemaMap[param.valueType] || toolValueTypeList[0].jsonSchema
: toolValueTypeList[0].jsonSchema;
const enumValues =
param.list?.map((item) => item.value).filter(Boolean) ||
param.enum?.split('\n').filter(Boolean);
const schema = {
...jsonSchema,
description: param.toolDescription || ''
};
if (!enumValues?.length) {
properties[param.key] = schema;
return;
}
properties[param.key] =
schema.type === 'array'
? {
...schema,
items: {
...(schema.items && typeof schema.items === 'object' ? schema.items : {}),
enum: enumValues
}
}
: {
...schema,
enum: enumValues
};
});
return { return {
type: 'function', type: 'function',
function: { function: {
name: formatToolId, name: formatToolId,
description, description,
parameters: { parameters: nodeInputs2JsonSchema({ inputs: toolParams })
type: 'object',
properties,
required: toolParams.filter((param) => param.required).map((param) => param.key)
}
} }
}; };
}; };
...@@ -558,7 +530,8 @@ export const getAgentRuntimeTools = async ({ ...@@ -558,7 +530,8 @@ export const getAgentRuntimeTools = async ({
toolSetNode: { toolSetNode: {
toolConfig: toolNode.toolConfig, toolConfig: toolNode.toolConfig,
inputs: toolNode.inputs, inputs: toolNode.inputs,
nodeId: pluginId nodeId: pluginId,
version: toolNode.version
}, },
lang lang
}); });
......
...@@ -4,8 +4,7 @@ import type { ...@@ -4,8 +4,7 @@ import type {
} from '@fastgpt/global/core/ai/llm/type'; } from '@fastgpt/global/core/ai/llm/type';
import { SANDBOX_SYSTEM_PROMPT } from '@fastgpt/global/core/ai/sandbox/constants'; import { SANDBOX_SYSTEM_PROMPT } from '@fastgpt/global/core/ai/sandbox/constants';
import { SANDBOX_TOOLS } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_TOOLS } from '@fastgpt/global/core/ai/sandbox/tools';
import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema'; import { nodeInputs2JsonSchema } from '@fastgpt/global/core/app/jsonschema';
import { toolValueTypeList, valueTypeJsonSchemaMap } from '@fastgpt/global/core/workflow/constants';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import type { localeType } from '@fastgpt/global/common/i18n/type'; import type { localeType } from '@fastgpt/global/common/i18n/type';
import { getSandboxToolInfo, injectSandboxFiles } from '../../../../../ai/sandbox/toolCall'; import { getSandboxToolInfo, injectSandboxFiles } from '../../../../../ai/sandbox/toolCall';
...@@ -42,29 +41,12 @@ const createToolSchema = (item: ToolNodeItemType): ChatCompletionTool => { ...@@ -42,29 +41,12 @@ const createToolSchema = (item: ToolNodeItemType): ChatCompletionTool => {
}; };
} }
const properties: Record<string, JsonSchemaPropertiesItemType> = {};
item.toolParams.forEach((input) => {
const jsonSchema = input.valueType
? valueTypeJsonSchemaMap[input.valueType] || toolValueTypeList[0].jsonSchema
: toolValueTypeList[0].jsonSchema;
properties[input.key] = {
...jsonSchema,
description: input.toolDescription || '',
enum: input.enum?.split('\n').filter(Boolean) || undefined
};
});
return { return {
type: 'function', type: 'function',
function: { function: {
name: item.nodeId, name: item.nodeId,
description: `${item.name}: ${item.toolDescription || item.intro}`, description: `${item.name}: ${item.toolDescription || item.intro}`,
parameters: { parameters: nodeInputs2JsonSchema({ inputs: item.toolParams })
type: 'object',
properties,
required: item.toolParams.filter((input) => input.required).map((input) => input.key)
}
} }
}; };
}; };
......
...@@ -161,15 +161,14 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -161,15 +161,14 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
} }
// Custom error field // Custom error field
return { return getNodeErrResponse({
error: res.error, error: res.error,
[DispatchNodeResponseKeyEnum.nodeResponse]: { [DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput, toolInput,
error: res.error, error: res.error,
moduleLogo: avatar moduleLogo: avatar
}, }
[DispatchNodeResponseKeyEnum.toolResponse]: res.error });
};
} }
const usagePoints = (() => { const usagePoints = (() => {
...@@ -287,18 +286,14 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -287,18 +286,14 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
}); });
if (errorMsg) { if (errorMsg) {
if (catchError) { return getNodeErrResponse({
return { error: errorMsg,
error: { [NodeOutputKeyEnum.errorText]: errorMsg }, [DispatchNodeResponseKeyEnum.nodeResponse]: {
[DispatchNodeResponseKeyEnum.nodeResponse]: { toolInput,
toolInput, toolRes: errorMsg,
toolRes: errorMsg, moduleLogo: avatar
moduleLogo: avatar }
}, });
[DispatchNodeResponseKeyEnum.toolResponse]: errorMsg
};
}
throw new Error(errorMsg);
} }
return { return {
......
...@@ -5,6 +5,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; ...@@ -5,6 +5,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { localeType } from '@fastgpt/global/common/i18n/type'; import type { localeType } from '@fastgpt/global/common/i18n/type';
import { SystemToolRepo } from '../../app/tool/systemTool/systemTool.repo'; import { SystemToolRepo } from '../../app/tool/systemTool/systemTool.repo';
import { jsonSchema2NodeInput, jsonSchema2NodeOutput } from '@fastgpt/global/core/app/jsonschema';
/* filter search result */ /* filter search result */
export const filterSearchResultsByMaxChars = async ( export const filterSearchResultsByMaxChars = async (
...@@ -74,18 +75,22 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ ...@@ -74,18 +75,22 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({
const pluginId = `${systemToolId}/${child.id}`; const pluginId = `${systemToolId}/${child.id}`;
const intro = selectedTool.description || child.description; const intro = selectedTool.description || child.description;
const toolDescription = selectedTool.description || child.toolDescription || child.description; const toolDescription = selectedTool.description || child.toolDescription || child.description;
const childInputs = jsonSchema2NodeInput({
jsonSchema: child.inputSchema,
schemaType: 'systemTool'
});
const childOutputs = jsonSchema2NodeOutput({ jsonSchema: child.outputSchema });
return { return {
flowNodeType: FlowNodeTypeEnum.tool, flowNodeType: FlowNodeTypeEnum.tool,
avatar: tool.avatar, avatar: tool.avatar,
inputs: toolsetInputConfig inputs: toolsetInputConfig ? [toolsetInputConfig, ...childInputs] : childInputs,
? [toolsetInputConfig, ...(child.inputs ?? [])] outputs: childOutputs,
: (child.inputs ?? []),
outputs: child.outputs ?? [],
name: selectedTool.name || child.name, name: selectedTool.name || child.name,
intro, intro,
nodeId: `${toolSetNode.nodeId}${child.id}`, nodeId: `${toolSetNode.nodeId}${child.id}`,
version: runtimeVersion, version: runtimeVersion,
jsonSchema: child.inputSchema,
toolDescription, toolDescription,
toolConfig: { toolConfig: {
systemTool: { systemTool: {
......
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SystemToolSystemSecretStatusEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import { SystemToolSystemSecretStatusEnum } from '@fastgpt/global/core/app/tool/systemTool/constants';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
listTools: vi.fn(), listTools: vi.fn(),
...@@ -224,7 +229,40 @@ describe('SystemToolRepo.getSystemToolDetail', () => { ...@@ -224,7 +229,40 @@ describe('SystemToolRepo.getSystemToolDetail', () => {
mocks.getAppLatestVersion.mockResolvedValue({ mocks.getAppLatestVersion.mockResolvedValue({
versionId: 'latest-version', versionId: 'latest-version',
versionName: 'Latest Version', versionName: 'Latest Version',
nodes: [] nodes: [
{
nodeId: 'plugin-input',
flowNodeType: FlowNodeTypeEnum.pluginInput,
name: 'Plugin Input',
inputs: [
{
key: 'query',
label: 'Query',
valueType: WorkflowIOValueTypeEnum.string,
toolDescription: 'Search query',
required: true,
renderTypeList: [FlowNodeInputTypeEnum.input]
}
],
outputs: []
},
{
nodeId: 'plugin-output',
flowNodeType: FlowNodeTypeEnum.pluginOutput,
name: 'Plugin Output',
inputs: [
{
key: 'result',
label: 'Result',
valueType: WorkflowIOValueTypeEnum.string,
description: 'Search result',
required: true,
renderTypeList: [FlowNodeInputTypeEnum.input]
}
],
outputs: []
}
]
}); });
mocks.checkIsLatestVersion.mockResolvedValue(true); mocks.checkIsLatestVersion.mockResolvedValue(true);
...@@ -236,6 +274,282 @@ describe('SystemToolRepo.getSystemToolDetail', () => { ...@@ -236,6 +274,282 @@ describe('SystemToolRepo.getSystemToolDetail', () => {
expect(tool.hasTokenFee).toBe(true); expect(tool.hasTokenFee).toBe(true);
expect(tool.version).toBe('latest-version'); expect(tool.version).toBe('latest-version');
expect(tool.versionLabel).toBe('Latest Version'); expect(tool.versionLabel).toBe('Latest Version');
expect(tool.inputSchema).toEqual({
type: 'object',
properties: {
query: {
type: 'string',
title: 'Query',
description: 'Search query',
toolDescription: 'Search query'
}
},
required: ['query']
});
expect(tool.outputSchema).toEqual({
type: 'object',
properties: {
result: {
type: 'string',
title: 'Result',
description: 'Search result'
}
},
required: ['result']
});
expect(tool).not.toHaveProperty('inputs');
expect(tool).not.toHaveProperty('outputs');
});
it('returns plugin client schemas directly for system tools', async () => {
const inputSchema = {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' }
},
required: ['city']
};
const outputSchema = {
type: 'object',
properties: {
weather: { type: 'string' }
}
};
const secretSchema = {
type: 'object',
properties: {
apiKey: { type: 'string', isSecret: true }
},
required: ['apiKey']
};
mocks.findSystemTool.mockResolvedValue({
pluginId: 'systemTool-weather',
status: 'Normal',
currentCost: 1,
hasTokenFee: false,
systemKeyCost: 2,
customConfig: {}
});
mocks.findSystemTools.mockResolvedValue([]);
mocks.getTool.mockResolvedValue({
source: 'system',
isToolset: true,
name: { en: 'Weather' },
description: { en: 'Weather intro' },
pluginId: 'weather',
version: '1.0.0',
icon: 'weather.svg',
tags: [],
toolDescription: 'Weather tool',
inputSchema,
outputSchema,
secretSchema,
children: [
{
id: 'forecast',
name: { en: 'Forecast' },
description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool',
inputSchema,
outputSchema
}
]
});
const tool = await SystemToolRepo.getInstance().getSystemToolDetail({
pluginId: 'systemTool-weather'
});
expect(tool.inputSchema).toBe(inputSchema);
expect(tool.outputSchema).toBe(outputSchema);
expect(tool.secretSchema).toBe(secretSchema);
expect(tool.children?.[0]).toMatchObject({
id: 'forecast',
inputSchema,
outputSchema
});
expect(tool).not.toHaveProperty('inputs');
expect(tool).not.toHaveProperty('outputs');
expect(tool).not.toHaveProperty('secrets');
});
});
describe('SystemToolRepo.getSystemToolDisplayInfo', () => {
it('returns workflow tool display metadata without loading app version schemas', async () => {
mocks.findSystemTool.mockResolvedValue({
pluginId: 'systemTool-workflow-tool',
status: 'Normal',
currentCost: 3,
hasTokenFee: true,
systemKeyCost: 4,
pluginOrder: 5,
originCost: 6,
hideTags: ['hidden-user'],
promoteTags: ['vip'],
customConfig: {
name: 'Workflow Tool',
avatar: 'workflow.svg',
intro: 'Workflow intro',
toolDescription: 'Workflow description',
version: 'workflow-version',
tags: ['workflow'],
associatedPluginId: 'app-id',
author: 'Custom Author',
userGuide: 'Guide'
}
});
const tool = await SystemToolRepo.getInstance().getSystemToolDisplayInfo({
pluginId: 'systemTool-workflow-tool'
});
expect(tool).toMatchObject({
id: 'systemTool-workflow-tool',
name: 'Workflow Tool',
avatar: 'workflow.svg',
intro: 'Workflow intro',
toolDescription: 'Workflow description',
version: 'workflow-version',
currentCost: 3,
systemKeyCost: 4,
hasTokenFee: true
});
expect(tool).not.toHaveProperty('inputSchema');
expect(tool).not.toHaveProperty('outputSchema');
expect(tool).not.toHaveProperty('secretSchema');
expect(mocks.findAppById).not.toHaveBeenCalled();
expect(mocks.getAppLatestVersion).not.toHaveBeenCalled();
expect(mocks.getTool).not.toHaveBeenCalled();
expect(mocks.listTools).not.toHaveBeenCalled();
});
it('returns toolset child display metadata without child schemas', async () => {
const inputSchema = {
type: 'object',
properties: {
city: { type: 'string' }
}
};
const outputSchema = {
type: 'object',
properties: {
weather: { type: 'string' }
}
};
mocks.findSystemTool.mockResolvedValue(undefined);
mocks.listTools.mockResolvedValue([
{
source: 'system',
isToolset: true,
name: { en: 'Weather' },
description: { en: 'Weather intro' },
pluginId: 'weather',
version: '1.0.0',
icon: 'weather.svg',
tags: ['life'],
toolDescription: 'Weather tool',
hasSecret: false,
children: [
{
id: 'forecast',
name: { en: 'Forecast' },
description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool',
icon: 'forecast.svg',
inputSchema,
outputSchema
}
]
}
]);
mocks.findSystemTools.mockResolvedValue([
{
pluginId: 'systemTool-weather/forecast',
currentCost: 2,
systemKeyCost: 1,
customConfig: {
name: 'Forecast',
version: '1.0.0',
toolDescription: 'Configured forecast'
}
}
]);
const tool = await SystemToolRepo.getInstance().getSystemToolDisplayInfo({
pluginId: 'systemTool-weather/forecast'
});
expect(tool).toMatchObject({
id: 'systemTool-weather/forecast',
name: 'Forecast',
intro: 'Forecast intro',
avatar: 'forecast.svg',
toolDescription: 'Configured forecast',
currentCost: 2,
systemKeyCost: 1
});
expect(tool).not.toHaveProperty('inputSchema');
expect(tool).not.toHaveProperty('outputSchema');
expect(tool).not.toHaveProperty('secretSchema');
expect(mocks.getTool).not.toHaveBeenCalled();
});
it('returns parent toolset children without schemas', async () => {
const inputSchema = {
type: 'object',
properties: {
city: { type: 'string' }
}
};
const outputSchema = {
type: 'object',
properties: {
weather: { type: 'string' }
}
};
mocks.findSystemTool.mockResolvedValue(undefined);
mocks.listTools.mockResolvedValue([
{
source: 'system',
isToolset: true,
name: { en: 'Weather' },
description: { en: 'Weather intro' },
pluginId: 'weather',
version: '1.0.0',
icon: 'weather.svg',
tags: ['life'],
toolDescription: 'Weather tool',
hasSecret: false,
children: [
{
id: 'forecast',
name: { en: 'Forecast' },
description: { en: 'Forecast intro' },
toolDescription: 'Forecast tool',
inputSchema,
outputSchema
}
]
}
]);
mocks.findSystemTools.mockResolvedValue([]);
const tool = await SystemToolRepo.getInstance().getSystemToolDisplayInfo({
pluginId: 'systemTool-weather'
});
expect(tool.children?.[0]).toMatchObject({
id: 'forecast',
name: 'Forecast',
description: 'Forecast intro',
toolDescription: 'Forecast tool'
});
expect(tool.children?.[0]).not.toHaveProperty('inputSchema');
expect(tool.children?.[0]).not.toHaveProperty('outputSchema');
expect(mocks.getTool).not.toHaveBeenCalled();
}); });
}); });
......
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const mocks = vi.hoisted(() => ({
findById: vi.fn(),
getAppVersionById: vi.fn()
}));
vi.mock('@fastgpt/service/core/app/schema', () => ({
MongoApp: {
findById: mocks.findById
}
}));
vi.mock('@fastgpt/service/core/app/version/controller', () => ({
getAppVersionById: mocks.getAppVersionById,
checkIsLatestVersion: vi.fn()
}));
vi.mock('@fastgpt/service/core/app/tool/systemTool/systemTool.repo', () => ({
SystemToolRepo: {
getInstance: vi.fn()
}
}));
import { getClientToolPreviewNode } from '@fastgpt/service/core/app/tool/utils/client';
describe('getClientToolPreviewNode', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('omits runtime schema fields from client preview response', async () => {
mocks.findById.mockReturnValueOnce({
lean: vi.fn().mockResolvedValue({
_id: '507f1f77bcf86cd799439011',
teamId: '507f1f77bcf86cd799439012',
type: AppTypeEnum.httpToolSet,
name: 'HTTP Tools',
avatar: 'http.svg',
intro: 'HTTP toolset'
})
});
mocks.getAppVersionById.mockResolvedValueOnce({
nodes: [
{
toolConfig: {
httpToolSet: {
toolList: [
{
name: 'search',
description: 'Search tool',
requestSchema: { type: 'object', properties: { q: { type: 'string' } } },
inputSchema: { type: 'object', properties: { q: { type: 'string' } } },
outputSchema: { type: 'object', properties: { result: { type: 'string' } } }
}
]
}
}
}
],
edges: [],
chatConfig: {},
versionId: 'version-id',
versionName: 'Version 1'
});
const result = await getClientToolPreviewNode({
appId: 'http-507f1f77bcf86cd799439011/search',
lang: 'en'
});
expect(result).not.toHaveProperty('jsonSchema');
expect(result).not.toHaveProperty('inputSchema');
expect(result).not.toHaveProperty('outputSchema');
expect(result).not.toHaveProperty('secretSchema');
expect(result.toolConfig?.httpTool).toEqual({
toolId: 'http-507f1f77bcf86cd799439011/search'
});
expect(result.inputs[0]?.key).toBe('q');
expect((result as any).jsonSchema).toBeUndefined();
});
});
...@@ -16,9 +16,9 @@ vi.mock('@fastgpt/service/core/app/tool/systemTool/systemTool.repo', () => ({ ...@@ -16,9 +16,9 @@ vi.mock('@fastgpt/service/core/app/tool/systemTool/systemTool.repo', () => ({
} }
})); }));
import { getToolPreviewNode } from '@fastgpt/service/core/app/tool/presenter'; import { getClientSystemToolPreviewNode } from '@fastgpt/service/core/app/tool/utils/client';
describe('getToolPreviewNode', () => { describe('getClientSystemToolPreviewNode', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mocks.getInstance.mockReturnValue({ mocks.getInstance.mockReturnValue({
...@@ -27,15 +27,6 @@ describe('getToolPreviewNode', () => { ...@@ -27,15 +27,6 @@ describe('getToolPreviewNode', () => {
}); });
it('adds system input config when system tool has secrets', async () => { it('adds system input config when system tool has secrets', async () => {
const secrets = [
{
key: 'apiKey',
label: 'API Key',
inputType: 'secret',
required: true
}
];
mocks.getSystemToolDetail.mockResolvedValueOnce({ mocks.getSystemToolDetail.mockResolvedValueOnce({
id: 'systemTool-weather', id: 'systemTool-weather',
version: '1.0.0', version: '1.0.0',
...@@ -52,20 +43,29 @@ describe('getToolPreviewNode', () => { ...@@ -52,20 +43,29 @@ describe('getToolPreviewNode', () => {
systemKeyCost: 1, systemKeyCost: 1,
hasTokenFee: false, hasTokenFee: false,
hasSystemSecret: true, hasSystemSecret: true,
secrets, secretSchema: {
inputs: [ type: 'object',
{ properties: {
key: 'city', apiKey: {
label: 'City', type: 'string',
valueType: 'string', title: 'API Key',
renderTypeList: [FlowNodeInputTypeEnum.input], isSecret: true
required: true }
},
required: ['apiKey']
},
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
title: 'City'
}
} }
], }
outputs: []
}); });
const result = await getToolPreviewNode({ const result = await getClientSystemToolPreviewNode({
pluginId: 'systemTool-weather', pluginId: 'systemTool-weather',
versionId: '1.0.0', versionId: '1.0.0',
lang: 'en' lang: 'en'
...@@ -75,7 +75,15 @@ describe('getToolPreviewNode', () => { ...@@ -75,7 +75,15 @@ describe('getToolPreviewNode', () => {
key: NodeInputKeyEnum.systemInputConfig, key: NodeInputKeyEnum.systemInputConfig,
label: '', label: '',
renderTypeList: [FlowNodeInputTypeEnum.hidden], renderTypeList: [FlowNodeInputTypeEnum.hidden],
inputList: secrets inputList: [
{
key: 'apiKey',
label: 'API Key',
inputType: 'secret',
description: undefined,
required: true
}
]
}); });
expect(result.inputs[1]?.key).toBe('city'); expect(result.inputs[1]?.key).toBe('city');
expect(result.version).toBe('1.0.0'); expect(result.version).toBe('1.0.0');
...@@ -99,19 +107,18 @@ describe('getToolPreviewNode', () => { ...@@ -99,19 +107,18 @@ describe('getToolPreviewNode', () => {
systemKeyCost: 0, systemKeyCost: 0,
hasTokenFee: false, hasTokenFee: false,
hasSystemSecret: false, hasSystemSecret: false,
inputs: [ inputSchema: {
{ type: 'object',
key: 'city', properties: {
label: 'City', city: {
valueType: 'string', type: 'string',
renderTypeList: [FlowNodeInputTypeEnum.input], title: 'City'
required: true }
} }
], }
outputs: []
}); });
const result = await getToolPreviewNode({ const result = await getClientSystemToolPreviewNode({
pluginId: 'systemTool-weather', pluginId: 'systemTool-weather',
lang: 'en' lang: 'en'
}); });
...@@ -138,12 +145,10 @@ describe('getToolPreviewNode', () => { ...@@ -138,12 +145,10 @@ describe('getToolPreviewNode', () => {
currentCost: 0, currentCost: 0,
systemKeyCost: 0, systemKeyCost: 0,
hasTokenFee: false, hasTokenFee: false,
hasSystemSecret: false, hasSystemSecret: false
inputs: [],
outputs: []
}); });
const result = await getToolPreviewNode({ const result = await getClientSystemToolPreviewNode({
pluginId: 'systemTool-weather', pluginId: 'systemTool-weather',
getLatestVersion: true, getLatestVersion: true,
lang: 'en' lang: 'en'
...@@ -169,12 +174,10 @@ describe('getToolPreviewNode', () => { ...@@ -169,12 +174,10 @@ describe('getToolPreviewNode', () => {
currentCost: 0, currentCost: 0,
systemKeyCost: 0, systemKeyCost: 0,
hasTokenFee: false, hasTokenFee: false,
hasSystemSecret: false, hasSystemSecret: false
inputs: [],
outputs: []
}); });
const result = await getToolPreviewNode({ const result = await getClientSystemToolPreviewNode({
pluginId: 'systemTool-weather', pluginId: 'systemTool-weather',
versionId: '', versionId: '',
lang: 'en' lang: 'en'
...@@ -209,19 +212,28 @@ describe('getToolPreviewNode', () => { ...@@ -209,19 +212,28 @@ describe('getToolPreviewNode', () => {
hasTokenFee: true, hasTokenFee: true,
hasSystemSecret: false, hasSystemSecret: false,
associatedPluginId: 'app-id', associatedPluginId: 'app-id',
inputs: [ inputSchema: {
{ type: 'object',
key: 'query', properties: {
label: 'Query', query: {
valueType: 'string', type: 'string',
renderTypeList: [FlowNodeInputTypeEnum.input], title: 'Query'
required: true }
},
required: ['query']
},
outputSchema: {
type: 'object',
properties: {
result: {
type: 'string',
title: 'Result'
}
} }
], }
outputs: []
}); });
const result = await getToolPreviewNode({ const result = await getClientSystemToolPreviewNode({
pluginId: 'commercial-workflow-tool', pluginId: 'commercial-workflow-tool',
versionId: 'workflow-version', versionId: 'workflow-version',
lang: 'en' lang: 'en'
......
...@@ -17,11 +17,6 @@ vi.mock('@fastgpt/service/core/ai/model', () => ({ ...@@ -17,11 +17,6 @@ vi.mock('@fastgpt/service/core/ai/model', () => ({
})) }))
})); }));
vi.mock('@fastgpt/service/core/app/tool/controller', () => ({
getSystemToolsWithInstalled: vi.fn(async () => []),
getMyTools: vi.fn(async () => [])
}));
vi.mock('@fastgpt/service/core/app/tool/workflowTool', () => ({ vi.mock('@fastgpt/service/core/app/tool/workflowTool', () => ({
getUserAvaliableWorkflowTools: vi.fn(async () => []) getUserAvaliableWorkflowTools: vi.fn(async () => [])
})); }));
......
...@@ -4,11 +4,6 @@ import { ...@@ -4,11 +4,6 @@ import {
SANDBOX_SHELL_TOOL_NAME SANDBOX_SHELL_TOOL_NAME
} from '@fastgpt/global/core/ai/sandbox/tools'; } from '@fastgpt/global/core/ai/sandbox/tools';
vi.mock('@fastgpt/service/core/app/tool/controller', () => ({
getSystemToolsWithInstalled: vi.fn(async () => []),
getMyTools: vi.fn(async () => [])
}));
vi.mock('@fastgpt/service/core/app/tool/workflowTool', () => ({ vi.mock('@fastgpt/service/core/app/tool/workflowTool', () => ({
getUserAvaliableWorkflowTools: vi.fn(async () => []) getUserAvaliableWorkflowTools: vi.fn(async () => [])
})); }));
......
...@@ -3,7 +3,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; ...@@ -3,7 +3,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getAgentRuntimeTools } from '@fastgpt/service/core/workflow/dispatch/ai/agent/sub/tool/utils'; import { getAgentRuntimeTools } from '@fastgpt/service/core/workflow/dispatch/ai/agent/sub/tool/utils';
import type { NodeToolConfigType } from '@fastgpt/global/core/workflow/type/node'; import type { NodeToolConfigType } from '@fastgpt/global/core/workflow/type/node';
import { jsonSchema2NodeInput } from '@fastgpt/global/core/app/jsonschema';
const { authAppByTmbIdMock, getAppVersionByIdMock, getSystemToolDetailMock } = vi.hoisted(() => ({ const { authAppByTmbIdMock, getAppVersionByIdMock, getSystemToolDetailMock } = vi.hoisted(() => ({
authAppByTmbIdMock: vi.fn(), authAppByTmbIdMock: vi.fn(),
...@@ -74,6 +73,29 @@ const httpRequestSchema = { ...@@ -74,6 +73,29 @@ const httpRequestSchema = {
required: ['body'] required: ['body']
}; };
const systemToolInputSchema = {
type: 'object',
additionalProperties: false,
properties: {
payload: {
type: 'object',
description: 'Structured payload',
properties: {
keyword: {
type: 'string',
pattern: '^[a-z]+$'
},
count: {
type: 'integer',
minimum: 1
}
},
required: ['keyword']
}
},
required: ['payload']
};
const mcpTool = { const mcpTool = {
name: 'search', name: 'search',
description: 'Search docs', description: 'Search docs',
...@@ -335,17 +357,13 @@ describe('getAgentRuntimeTools schema loading', () => { ...@@ -335,17 +357,13 @@ describe('getAgentRuntimeTools schema loading', () => {
author: '', author: '',
version: '1.0.0', version: '1.0.0',
isLatestVersion: true, isLatestVersion: true,
outputs: [],
inputs: [],
children: [ children: [
{ {
id: 'search', id: 'search',
name: '搜索网页', name: '搜索网页',
description: '搜索网页内容', description: '搜索网页内容',
currentCost: 0, currentCost: 0,
systemKeyCost: 0, systemKeyCost: 0
inputs: [],
outputs: []
} }
] ]
}); });
...@@ -363,7 +381,7 @@ describe('getAgentRuntimeTools schema loading', () => { ...@@ -363,7 +381,7 @@ describe('getAgentRuntimeTools schema loading', () => {
}); });
}); });
it('loads system tool params from standard JSON schema description', async () => { it('loads system tool params from its original input schema', async () => {
getSystemToolDetailMock.mockResolvedValue({ getSystemToolDetailMock.mockResolvedValue({
id: 'systemTool-gpjj5s', id: 'systemTool-gpjj5s',
name: '热榜工具', name: '热榜工具',
...@@ -382,25 +400,38 @@ describe('getAgentRuntimeTools schema loading', () => { ...@@ -382,25 +400,38 @@ describe('getAgentRuntimeTools schema loading', () => {
author: '', author: '',
version: '1.0.0', version: '1.0.0',
isLatestVersion: true, isLatestVersion: true,
outputs: [], inputSchema: systemToolInputSchema
inputs: jsonSchema2NodeInput({ });
schemaType: 'systemTool',
jsonSchema: { const tools = await getAgentRuntimeTools({
type: 'object', tmbId: 'tmb_1',
properties: { tools: [{ id: 'systemTool-gpjj5s', config: {} }]
platform: { });
type: 'array',
title: '热榜平台', expect(tools).toHaveLength(1);
description: '选择热榜来源网站(可多选)', expect(tools[0].requestSchema.function.name).toBe('gpjj5s');
items: { expect(tools[0].requestSchema.function.parameters).toBe(systemToolInputSchema);
type: 'string', });
enum: ['36kr', 'zhihu', 'weibo', 'juejin', 'toutiao']
} it('does not rebuild system tool params from node inputs when input schema is missing', async () => {
} getSystemToolDetailMock.mockResolvedValue({
}, id: 'systemTool-gpjj5s',
required: ['platform'] name: '热榜工具',
} avatar: 'hot-list.png',
}) intro: '获取热榜信息,支持36氪、知乎、微博、掘金、头条等多个平台',
toolDescription: '获取热榜信息',
status: 'active',
source: 'system',
isToolSet: false,
hasSystemSecret: false,
systemSecretStatus: 'none',
currentCost: 0,
systemKeyCost: 0,
hasTokenFee: false,
tags: [],
author: '',
version: '1.0.0',
isLatestVersion: true
}); });
const tools = await getAgentRuntimeTools({ const tools = await getAgentRuntimeTools({
...@@ -412,17 +443,7 @@ describe('getAgentRuntimeTools schema loading', () => { ...@@ -412,17 +443,7 @@ describe('getAgentRuntimeTools schema loading', () => {
expect(tools[0].requestSchema.function.name).toBe('gpjj5s'); expect(tools[0].requestSchema.function.name).toBe('gpjj5s');
expect(tools[0].requestSchema.function.parameters).toEqual({ expect(tools[0].requestSchema.function.parameters).toEqual({
type: 'object', type: 'object',
properties: { properties: {}
platform: {
type: 'array',
items: {
type: 'string',
enum: ['36kr', 'zhihu', 'weibo', 'juejin', 'toutiao']
},
description: '选择热榜来源网站(可多选)'
}
},
required: ['platform']
}); });
}); });
}); });
...@@ -74,7 +74,10 @@ describe('useToolCatalog', () => { ...@@ -74,7 +74,10 @@ describe('useToolCatalog', () => {
key: 'city', key: 'city',
valueType: WorkflowIOValueTypeEnum.string, valueType: WorkflowIOValueTypeEnum.string,
toolDescription: 'City name', toolDescription: 'City name',
enum: 'Hangzhou\nShanghai\n', list: [
{ label: 'Hangzhou', value: 'Hangzhou' },
{ label: 'Shanghai', value: 'Shanghai' }
],
required: true required: true
}, },
{ {
...@@ -120,13 +123,16 @@ describe('useToolCatalog', () => { ...@@ -120,13 +123,16 @@ describe('useToolCatalog', () => {
properties: { properties: {
city: { city: {
type: 'string', type: 'string',
title: 'city',
description: 'City name', description: 'City name',
toolDescription: 'City name',
enum: ['Hangzhou', 'Shanghai'] enum: ['Hangzhou', 'Shanghai']
}, },
days: { days: {
type: 'string', type: 'string',
description: 'Days', description: 'Days',
enum: undefined title: 'days',
toolDescription: 'Days'
} }
}, },
required: ['city'] required: ['city']
......
...@@ -3,6 +3,28 @@ import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/ty ...@@ -3,6 +3,28 @@ import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/ty
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
const childInputSchema = {
type: 'object',
properties: {
input1: {
type: 'string',
title: 'Input 1',
description: 'Input desc'
}
}
};
const childOutputSchema = {
type: 'object',
properties: {
output1: {
type: 'string',
title: 'Output 1',
description: 'Output desc'
}
}
};
// Mock token counting (uses worker threads, not available in test) // Mock token counting (uses worker threads, not available in test)
vi.mock('@fastgpt/service/common/string/tiktoken/index', () => ({ vi.mock('@fastgpt/service/common/string/tiktoken/index', () => ({
countPromptTokensBatch: vi.fn(async (texts: string[]) => { countPromptTokensBatch: vi.fn(async (texts: string[]) => {
...@@ -160,16 +182,16 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -160,16 +182,16 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
name?: string; name?: string;
description?: string; description?: string;
toolDescription?: string; toolDescription?: string;
inputs?: any[] | null; inputSchema?: any;
outputs?: any[] | null; outputSchema?: any;
}> = [ }> = [
{ {
id: 'child-1', id: 'child-1',
name: 'Original', name: 'Original',
description: 'Original Desc', description: 'Original Desc',
toolDescription: 'Original Tool Desc', toolDescription: 'Original Tool Desc',
inputs: [{ key: 'input1', value: 'val1' }], inputSchema: childInputSchema,
outputs: [{ key: 'output1' }] outputSchema: childOutputSchema
} }
], ],
version = 'v1' version = 'v1'
...@@ -204,15 +226,13 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -204,15 +226,13 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
name: 'Original', name: 'Original',
description: 'Original Desc', description: 'Original Desc',
toolDescription: 'Original Tool Desc', toolDescription: 'Original Tool Desc',
inputs: [{ key: 'input1', value: 'val1' }], inputSchema: childInputSchema,
outputs: [{ key: 'output1' }] outputSchema: childOutputSchema
}, },
{ {
id: 'child-2', id: 'child-2',
name: 'Not Selected', name: 'Not Selected',
description: 'Not Selected Desc', description: 'Not Selected Desc'
inputs: [],
outputs: []
} }
]); ]);
...@@ -238,11 +258,54 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -238,11 +258,54 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
systemTool: { toolId: 'systemTool-toolset-1/child-1' } systemTool: { toolId: 'systemTool-toolset-1/child-1' }
}); });
expect(result[0].pluginId).toBe('systemTool-toolset-1/child-1'); expect(result[0].pluginId).toBe('systemTool-toolset-1/child-1');
expect(result[0].inputs).toEqual([{ key: 'input1', value: 'val1' }]); expect(result[0].inputs[0]).toMatchObject({
expect(result[0].outputs).toEqual([{ key: 'output1' }]); key: 'input1',
label: 'Input 1',
valueType: 'string'
});
expect(result[0].outputs[0]).toMatchObject({
key: 'output1',
label: 'Output 1',
valueType: 'string'
});
expect(result[0].version).toBe('v1'); expect(result[0].version).toBe('v1');
}); });
it('should preserve original child input schema on runtime node', async () => {
const inputSchema = {
type: 'object',
additionalProperties: false,
properties: {
query: {
type: 'string',
pattern: '^fastgpt'
}
},
required: ['query']
};
const toolSetNode = makeToolSetNode({
toolId: 'systemTool-toolset-1',
toolList: [{ toolId: 'child-1', name: 'Search', description: 'Search Desc' }]
});
mockToolDetail([
{
id: 'child-1',
name: 'Search',
description: 'Search Intro',
inputSchema
}
]);
const result = await getSystemToolRunTimeNodeFromSystemToolset({
toolSetNode,
lang: 'en'
});
expect(result).toHaveLength(1);
expect(result[0].jsonSchema).toBe(inputSchema);
});
it('should use child name and descriptions when selected config is empty', async () => { it('should use child name and descriptions when selected config is empty', async () => {
const toolSetNode = makeToolSetNode({ const toolSetNode = makeToolSetNode({
toolList: [{ toolId: 'child-1', name: '', description: '' }] toolList: [{ toolId: 'child-1', name: '', description: '' }]
...@@ -253,9 +316,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -253,9 +316,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
id: 'child-1', id: 'child-1',
name: 'Original Name', name: 'Original Name',
description: 'Original Intro', description: 'Original Intro',
toolDescription: 'Original Tool Description', toolDescription: 'Original Tool Description'
inputs: [],
outputs: []
} }
]); ]);
...@@ -276,17 +337,12 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -276,17 +337,12 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
inputs: [{ key: NodeInputKeyEnum.systemInputConfig, value: { apiKey: 'test-key' } }] inputs: [{ key: NodeInputKeyEnum.systemInputConfig, value: { apiKey: 'test-key' } }]
}); });
const childInputs = [
{ key: NodeInputKeyEnum.systemInputConfig, value: null },
{ key: 'otherInput', value: 'other' }
];
mockToolDetail([ mockToolDetail([
{ {
id: 'child-1', id: 'child-1',
name: 'Tool1', name: 'Tool1',
description: 'Intro1', description: 'Intro1',
inputs: childInputs, inputSchema: childInputSchema
outputs: []
} }
]); ]);
...@@ -303,7 +359,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -303,7 +359,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
expect(configInput?.value).toEqual({ apiKey: 'test-key' }); expect(configInput?.value).toEqual({ apiKey: 'test-key' });
}); });
it('should not modify inputs when no systemInputConfig in toolSetNode', async () => { it('should not add systemInputConfig when no systemInputConfig in toolSetNode', async () => {
const toolSetNode = makeToolSetNode({ const toolSetNode = makeToolSetNode({
toolList: [{ toolId: 'child-1', name: 'Tool1', description: 'Desc1' }], toolList: [{ toolId: 'child-1', name: 'Tool1', description: 'Desc1' }],
inputs: [] // No systemInputConfig inputs: [] // No systemInputConfig
...@@ -314,8 +370,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -314,8 +370,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
id: 'child-1', id: 'child-1',
name: 'Tool1', name: 'Tool1',
description: 'Intro1', description: 'Intro1',
inputs: [{ key: NodeInputKeyEnum.systemInputConfig, value: 'original' }], inputSchema: childInputSchema
outputs: []
} }
]); ]);
...@@ -324,11 +379,10 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -324,11 +379,10 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
lang: 'en' lang: 'en'
}); });
// toolsetInputConfig is undefined, so value check is falsy → no modification
const configInput = result[0].inputs.find( const configInput = result[0].inputs.find(
(i: any) => i.key === NodeInputKeyEnum.systemInputConfig (i: any) => i.key === NodeInputKeyEnum.systemInputConfig
); );
expect(configInput?.value).toBe('original'); expect(configInput).toBeUndefined();
}); });
it('should use default lang "en" when not provided', async () => { it('should use default lang "en" when not provided', async () => {
...@@ -351,7 +405,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -351,7 +405,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
}); });
it('should handle null inputs/outputs from tool', async () => { it('should handle missing schemas from tool', async () => {
const toolSetNode = makeToolSetNode({ const toolSetNode = makeToolSetNode({
toolList: [{ toolId: 'child-1', name: 'Tool1', description: 'Desc1' }] toolList: [{ toolId: 'child-1', name: 'Tool1', description: 'Desc1' }]
}); });
...@@ -360,9 +414,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -360,9 +414,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
{ {
id: 'child-1', id: 'child-1',
name: 'Tool1', name: 'Tool1',
description: 'Intro1', description: 'Intro1'
inputs: null,
outputs: null
} }
]); ]);
...@@ -385,8 +437,8 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { ...@@ -385,8 +437,8 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => {
}); });
mockToolDetail([ mockToolDetail([
{ id: 'child-1', name: 'Original T1', inputs: [], outputs: [] }, { id: 'child-1', name: 'Original T1' },
{ id: 'child-2', name: 'Original T2', inputs: [], outputs: [] } { id: 'child-2', name: 'Original T2' }
]); ]);
const result = await getSystemToolRunTimeNodeFromSystemToolset({ const result = await getSystemToolRunTimeNodeFromSystemToolset({
......
{ {
"AI_input_is_empty": "The content passed to the AI ​​node is empty", "AI_input_is_empty": "The content passed to the AI ​​node is empty",
"LLM_model_response_empty": "The model response is empty, please check the model details request", "LLM_model_response_empty": "The model response is empty, please check the model details request",
"no_output_content": "The app returned no output",
"Next": "Next", "Next": "Next",
"Previous": "Previous", "Previous": "Previous",
"agent_plan_generating": "Creating plan", "agent_plan_generating": "Creating plan",
...@@ -11,6 +10,7 @@ ...@@ -11,6 +10,7 @@
"agent_plan_updating": "Updating plan", "agent_plan_updating": "Updating plan",
"ai_chat_loading": "AI Chat", "ai_chat_loading": "AI Chat",
"ai_reasoning": "Thinking process", "ai_reasoning": "Thinking process",
"all_apps.title": "Apps",
"back_to_text": "Text input", "back_to_text": "Text input",
"balance_not_enough_pause": "Workflow paused due to insufficient AI points", "balance_not_enough_pause": "Workflow paused due to insufficient AI points",
"chat.quote.No Data": "The file cannot be found", "chat.quote.No Data": "The file cannot be found",
...@@ -32,23 +32,21 @@ ...@@ -32,23 +32,21 @@
"completion_finish_null": "unknown", "completion_finish_null": "unknown",
"completion_finish_stop": "Completed normally", "completion_finish_stop": "Completed normally",
"completion_finish_tool_calls": "Tool calls", "completion_finish_tool_calls": "Tool calls",
"feedback_helpful": "Helpful",
"feedback_unhelpful": "Not helpful",
"compress_llm_messages": "Context compression", "compress_llm_messages": "Context compression",
"confirm_clear_input_value": "Are you sure to clear the form content? \nDefault values ​​will be restored!", "confirm_clear_input_value": "Are you sure to clear the form content? \nDefault values ​​will be restored!",
"mobile_clear_history_confirm_title": "Clear history?",
"mobile_clear_history_confirm_tip": "This action cannot be undone",
"confirm_to_clear_share_chat_history": "Are you sure you want to clear all chat history?", "confirm_to_clear_share_chat_history": "Are you sure you want to clear all chat history?",
"content_empty": "No Content", "content_empty": "No Content",
"continue_run": "continue running", "continue_run": "continue running",
"dataset_quote_type error": "Knowledge base reference type is wrong, correct type: { datasetId: string }[]", "dataset_quote_type error": "Knowledge base reference type is wrong, correct type: { datasetId: string }[]",
"dataset_search": "Dataset Search", "dataset_search": "Dataset Search",
"download_chunks": "Download data", "download_chunks": "Download data",
"empty_directory": "This directory is empty~",
"edit_message": "Edit message", "edit_message": "Edit message",
"empty_directory": "This directory is empty~",
"feedback_helpful": "Helpful",
"feedback_unhelpful": "Not helpful",
"file_amount_over": "Exceeded maximum file quantity {{max}}", "file_amount_over": "Exceeded maximum file quantity {{max}}",
"history_generating": "Thinking",
"go_to_dataset": "Go to knowledge base", "go_to_dataset": "Go to knowledge base",
"history_generating": "Thinking",
"history_slider.home.title": "chat", "history_slider.home.title": "chat",
"home.chat_app": "HomeChat-{{name}}", "home.chat_app": "HomeChat-{{name}}",
"home.select_tools": "Select Tool", "home.select_tools": "Select Tool",
...@@ -66,11 +64,16 @@ ...@@ -66,11 +64,16 @@
"log.feedback.read": "Read", "log.feedback.read": "Read",
"log.feedback.show_feedback": "Show Feedback", "log.feedback.show_feedback": "Show Feedback",
"master_agent_call": "Master agent", "master_agent_call": "Master agent",
"mobile_clear_history_confirm_tip": "This action cannot be undone",
"mobile_clear_history_confirm_title": "Clear history?",
"module_runtime_and": "Total Module Runtime", "module_runtime_and": "Total Module Runtime",
"no_output_content": "The app returned no output",
"plan_agent": "Plan agent", "plan_agent": "Plan agent",
"plan_check_tip": "Plan generated. Continue the chat to refine the plan", "plan_check_tip": "Plan generated. Continue the chat to refine the plan",
"plugins_output": "Plugin Output", "plugins_output": "Plugin Output",
"press_to_speak": "Hold down to speak", "press_to_speak": "Hold down to speak",
"processed": "Processed",
"processing": "Processing",
"quote_result_notice": "Results only reflect this search. Please note that information may become outdated.", "quote_result_notice": "Results only reflect this search. Please note that information may become outdated.",
"quote_source_title": "Citation sources", "quote_source_title": "Citation sources",
"read_file": "File parsing", "read_file": "File parsing",
...@@ -151,41 +154,54 @@ ...@@ -151,41 +154,54 @@
"response.user_select_result": "User Selection Result", "response.user_select_result": "User Selection Result",
"response_content": "Response Content", "response_content": "Response Content",
"resume_placeholder_generating": "Generating", "resume_placeholder_generating": "Generating",
"run_detail": "Run Details",
"sandbox_audio_not_supported": "Your browser does not support audio playback.", "sandbox_audio_not_supported": "Your browser does not support audio playback.",
"sandbox_binary_file_no_preview": "The file cannot be previewed, please download and view it directly", "sandbox_binary_file_no_preview": "The file cannot be previewed, please download and view it directly",
"sandbox_collapse_all": "Collapse all", "sandbox_collapse_all": "Collapse all",
"sandbox_confirm_delete_content": "Delete <name>{{name}}</name>? This action cannot be undone and will permanently delete all included files.", "sandbox_confirm_delete_content": "Delete <name>{{name}}</name>? This action cannot be undone and will permanently delete all included files.",
"sandbox_confirm_delete_title": "Confirm delete", "sandbox_confirm_delete_title": "Confirm delete",
"sandbox_connect_failed_max_attempts": "Too many failed attempts to connect to the sandbox. Connection stopped.", "sandbox_connect_failed_max_attempts": "Too many failed attempts to connect to the sandbox. Connection stopped.",
"sandbox_refresh_page_to_retry": "Please refresh the page to try again.",
"sandbox_create_failed": "Create failed", "sandbox_create_failed": "Create failed",
"sandbox_delete": "Delete", "sandbox_delete": "Delete",
"sandbox_delete_failed": "Delete failed", "sandbox_delete_failed": "Delete failed",
"sandbox_download": "Download", "sandbox_download": "Download",
"sandbox_download_failed": "Download failed", "sandbox_download_failed": "Download failed",
"sandbox_entry_tooltip": "View Sandbox files", "sandbox_entry_tooltip": "View Sandbox files",
"sandbox_file_already_exists": "A file or folder with the same name already exists in the target directory",
"sandbox_file_config": "Files", "sandbox_file_config": "Files",
"sandbox_file_loading": "Loading files (2/2)",
"sandbox_files": "Sandbox files", "sandbox_files": "Sandbox files",
"sandbox_html_preview": "Preview", "sandbox_html_preview": "Preview",
"sandbox_html_preview_failed": "Failed to generate preview link", "sandbox_html_preview_failed": "Failed to generate preview link",
"sandbox_load_failed": "Failed to load directory", "sandbox_load_failed": "Failed to load directory",
"sandbox_starting": "Starting sandbox (1/2)",
"sandbox_file_loading": "Loading files (2/2)",
"sandbox_move_failed": "Move failed", "sandbox_move_failed": "Move failed",
"sandbox_move_into_self_forbidden": "Cannot move a directory into itself or one of its subdirectories", "sandbox_move_into_self_forbidden": "Cannot move a directory into itself or one of its subdirectories",
"sandbox_file_already_exists": "A file or folder with the same name already exists in the target directory",
"sandbox_new_file": "New file", "sandbox_new_file": "New file",
"sandbox_new_file_placeholder": "New file...", "sandbox_new_file_placeholder": "New file...",
"sandbox_new_folder": "New folder", "sandbox_new_folder": "New folder",
"sandbox_new_folder_placeholder": "New folder...", "sandbox_new_folder_placeholder": "New folder...",
"sandbox_no_file": "There are no files in the sandbox yet", "sandbox_no_file": "There are no files in the sandbox yet",
"sandbox_refresh_page_to_retry": "Please refresh the page to try again.",
"sandbox_rename": "Rename", "sandbox_rename": "Rename",
"sandbox_rename_failed": "Rename failed", "sandbox_rename_failed": "Rename failed",
"sandbox_root_directory": "Root directory", "sandbox_root_directory": "Root directory",
"sandbox_save_failed": "Save failed",
"sandbox_search_files": "Search files", "sandbox_search_files": "Search files",
"sandbox_select_file_edit": "Select a file to edit", "sandbox_select_file_edit": "Select a file to edit",
"sandbox_save_failed": "Save failed", "sandbox_starting": "Starting sandbox (1/2)",
"sandbox_status_checkExisting": "Checking sandbox...",
"sandbox_status_connecting": "Connecting to sandbox...",
"sandbox_status_creatingContainer": "Initializing cloud sandbox...",
"sandbox_status_deployingSkills": "Deploying skill {{skillName}}...", "sandbox_status_deployingSkills": "Deploying skill {{skillName}}...",
"sandbox_status_downloadingPackage": "Downloading skill package...",
"sandbox_status_extractingPackage": "Extracting skill package...",
"sandbox_status_failed": "Sandbox creation failed",
"sandbox_status_failed_with_message": "Sandbox creation failed: {{message}}",
"sandbox_status_fetchSkills": "Fetching skill metadata...",
"sandbox_status_lazyInit": "Preparing sandbox...",
"sandbox_status_ready_cold": "Sandbox is ready",
"sandbox_status_ready_warm": "Sandbox is ready (warm start)",
"sandbox_status_uploadingPackage": "Uploading skill package to sandbox...",
"sandbox_terminal": "Terminal", "sandbox_terminal": "Terminal",
"sandbox_upload_failed": "Upload failed", "sandbox_upload_failed": "Upload failed",
"sandbox_upload_file": "Upload file", "sandbox_upload_file": "Upload file",
...@@ -248,7 +264,6 @@ ...@@ -248,7 +264,6 @@
"setting.save": "Save", "setting.save": "Save",
"setting.save_success": "Save successfully", "setting.save_success": "Save successfully",
"sidebar.all_apps": "All Apps", "sidebar.all_apps": "All Apps",
"all_apps.title": "Apps",
"sidebar.favourite_apps": "Favourite Apps", "sidebar.favourite_apps": "Favourite Apps",
"sidebar.home": "Home", "sidebar.home": "Home",
"sidebar.team_apps": "Team Apps", "sidebar.team_apps": "Team Apps",
...@@ -259,6 +274,5 @@ ...@@ -259,6 +274,5 @@
"unsupported_file_type": "Unsupported file types", "unsupported_file_type": "Unsupported file types",
"variable_invisable_in_share": "External variables are not visible in login-free links", "variable_invisable_in_share": "External variables are not visible in login-free links",
"view_all_citations": "View all", "view_all_citations": "View all",
"view_citations": "View References", "view_citations": "View References"
"run_detail": "Run Details"
} }
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
"permission.only_collaborators": "Collaborators Only", "permission.only_collaborators": "Collaborators Only",
"permission.team_read": "Team Read Access", "permission.team_read": "Team Read Access",
"permission.team_write": "Team Write Access", "permission.team_write": "Team Write Access",
"promotion.pay": "Payment",
"promotion.register": "Registration",
"register.confirm": "Confirm Registration", "register.confirm": "Confirm Registration",
"register.register_account": "Register {{account}} Account", "register.register_account": "Register {{account}} Account",
"register.success": "Registration Successful", "register.success": "Registration Successful",
......
{ {
"AI_input_is_empty": "传入 AI 节点的内容为空", "AI_input_is_empty": "传入 AI 节点的内容为空",
"LLM_model_response_empty": "模型响应为空,请查看模型详情请求", "LLM_model_response_empty": "模型响应为空,请查看模型详情请求",
"no_output_content": "应用无输出内容",
"Next": "下一个", "Next": "下一个",
"Previous": "上一个", "Previous": "上一个",
"agent_plan_generating": "正在生成计划", "agent_plan_generating": "正在生成计划",
...@@ -11,6 +10,7 @@ ...@@ -11,6 +10,7 @@
"agent_plan_updating": "正在更新计划", "agent_plan_updating": "正在更新计划",
"ai_chat_loading": "AI 对话", "ai_chat_loading": "AI 对话",
"ai_reasoning": "思考过程", "ai_reasoning": "思考过程",
"all_apps.title": "应用",
"back_to_text": "返回输入", "back_to_text": "返回输入",
"balance_not_enough_pause": "由于 AI 积分不足,暂停运行工作流", "balance_not_enough_pause": "由于 AI 积分不足,暂停运行工作流",
"chat.quote.No Data": "找不到该文件", "chat.quote.No Data": "找不到该文件",
...@@ -32,23 +32,21 @@ ...@@ -32,23 +32,21 @@
"completion_finish_null": "未知", "completion_finish_null": "未知",
"completion_finish_stop": "正常完成", "completion_finish_stop": "正常完成",
"completion_finish_tool_calls": "工具调用", "completion_finish_tool_calls": "工具调用",
"feedback_helpful": "有帮助",
"feedback_unhelpful": "没有帮助",
"compress_llm_messages": "上下文压缩", "compress_llm_messages": "上下文压缩",
"confirm_clear_input_value": "确认清空表单内容?将会恢复默认值!", "confirm_clear_input_value": "确认清空表单内容?将会恢复默认值!",
"mobile_clear_history_confirm_title": "确定清空历史记录?",
"mobile_clear_history_confirm_tip": "此操作不可撤销",
"confirm_to_clear_share_chat_history": "确认清空所有聊天记录?", "confirm_to_clear_share_chat_history": "确认清空所有聊天记录?",
"content_empty": "内容为空", "content_empty": "内容为空",
"continue_run": "继续运行", "continue_run": "继续运行",
"dataset_quote_type error": "知识库引用类型错误,正确类型:{ datasetId: string }[]", "dataset_quote_type error": "知识库引用类型错误,正确类型:{ datasetId: string }[]",
"dataset_search": "知识库检索", "dataset_search": "知识库检索",
"download_chunks": "下载数据", "download_chunks": "下载数据",
"empty_directory": "这个目录已经没东西可选了~",
"edit_message": "编辑消息", "edit_message": "编辑消息",
"empty_directory": "这个目录已经没东西可选了~",
"feedback_helpful": "有帮助",
"feedback_unhelpful": "没有帮助",
"file_amount_over": "超出最大文件数量 {{max}}", "file_amount_over": "超出最大文件数量 {{max}}",
"history_generating": "思考中",
"go_to_dataset": "前往知识库", "go_to_dataset": "前往知识库",
"history_generating": "思考中",
"history_slider.home.title": "聊天", "history_slider.home.title": "聊天",
"home.chat_app": "首页聊天", "home.chat_app": "首页聊天",
"home.select_tools": "选择工具", "home.select_tools": "选择工具",
...@@ -66,11 +64,16 @@ ...@@ -66,11 +64,16 @@
"log.feedback.read": "已读", "log.feedback.read": "已读",
"log.feedback.show_feedback": "显示反馈", "log.feedback.show_feedback": "显示反馈",
"master_agent_call": "主 agent 调用", "master_agent_call": "主 agent 调用",
"mobile_clear_history_confirm_tip": "此操作不可撤销",
"mobile_clear_history_confirm_title": "确定清空历史记录?",
"module_runtime_and": "工作流总运行时间", "module_runtime_and": "工作流总运行时间",
"no_output_content": "应用无输出内容",
"plan_agent": "规划 Agent", "plan_agent": "规划 Agent",
"plan_check_tip": "规划已生成,您可继续对话来微调当前规划", "plan_check_tip": "规划已生成,您可继续对话来微调当前规划",
"plugins_output": "插件输出", "plugins_output": "插件输出",
"press_to_speak": "按住说话", "press_to_speak": "按住说话",
"processed": "已处理",
"processing": "处理中",
"quote_result_notice": "仅代表本次搜索时的结果,请注意信息的时效性", "quote_result_notice": "仅代表本次搜索时的结果,请注意信息的时效性",
"quote_source_title": "引用来源", "quote_source_title": "引用来源",
"read_file": "文件解析", "read_file": "文件解析",
...@@ -151,41 +154,54 @@ ...@@ -151,41 +154,54 @@
"response.user_select_result": "用户选择结果", "response.user_select_result": "用户选择结果",
"response_content": "响应内容", "response_content": "响应内容",
"resume_placeholder_generating": "生成中", "resume_placeholder_generating": "生成中",
"run_detail": "运行详情",
"sandbox_audio_not_supported": "当前浏览器不支持音频播放。", "sandbox_audio_not_supported": "当前浏览器不支持音频播放。",
"sandbox_binary_file_no_preview": "无法预览该文件,请直接下载查看", "sandbox_binary_file_no_preview": "无法预览该文件,请直接下载查看",
"sandbox_collapse_all": "全部折叠", "sandbox_collapse_all": "全部折叠",
"sandbox_confirm_delete_content": "确定要删除 <name>{{name}}</name> 吗?此操作不可撤销,并将永久删除所有包含的子文件。", "sandbox_confirm_delete_content": "确定要删除 <name>{{name}}</name> 吗?此操作不可撤销,并将永久删除所有包含的子文件。",
"sandbox_confirm_delete_title": "确认删除", "sandbox_confirm_delete_title": "确认删除",
"sandbox_connect_failed_max_attempts": "连接沙盒失败次数过多,已停止尝试。", "sandbox_connect_failed_max_attempts": "连接沙盒失败次数过多,已停止尝试。",
"sandbox_refresh_page_to_retry": "请刷新页面重试。",
"sandbox_create_failed": "新建失败", "sandbox_create_failed": "新建失败",
"sandbox_delete": "删除", "sandbox_delete": "删除",
"sandbox_delete_failed": "删除失败", "sandbox_delete_failed": "删除失败",
"sandbox_download": "下载", "sandbox_download": "下载",
"sandbox_download_failed": "下载失败", "sandbox_download_failed": "下载失败",
"sandbox_entry_tooltip": "查看虚拟机文件", "sandbox_entry_tooltip": "查看虚拟机文件",
"sandbox_file_already_exists": "目标目录中已存在同名文件或文件夹",
"sandbox_file_config": "文件配置", "sandbox_file_config": "文件配置",
"sandbox_file_loading": "文件加载中(2/2)",
"sandbox_files": "虚拟机文件", "sandbox_files": "虚拟机文件",
"sandbox_html_preview": "预览", "sandbox_html_preview": "预览",
"sandbox_html_preview_failed": "预览链接生成失败", "sandbox_html_preview_failed": "预览链接生成失败",
"sandbox_load_failed": "加载目录失败", "sandbox_load_failed": "加载目录失败",
"sandbox_starting": "沙盒启动中(1/2)",
"sandbox_file_loading": "文件加载中(2/2)",
"sandbox_move_failed": "移动失败", "sandbox_move_failed": "移动失败",
"sandbox_move_into_self_forbidden": "不能将目录移入其自身或其子目录下", "sandbox_move_into_self_forbidden": "不能将目录移入其自身或其子目录下",
"sandbox_file_already_exists": "目标目录中已存在同名文件或文件夹",
"sandbox_new_file": "新建文件", "sandbox_new_file": "新建文件",
"sandbox_new_file_placeholder": "新建文件...", "sandbox_new_file_placeholder": "新建文件...",
"sandbox_new_folder": "新建文件夹", "sandbox_new_folder": "新建文件夹",
"sandbox_new_folder_placeholder": "新建文件夹...", "sandbox_new_folder_placeholder": "新建文件夹...",
"sandbox_no_file": "虚拟机里还没有文件", "sandbox_no_file": "虚拟机里还没有文件",
"sandbox_refresh_page_to_retry": "请刷新页面重试。",
"sandbox_rename": "重命名", "sandbox_rename": "重命名",
"sandbox_rename_failed": "重命名失败", "sandbox_rename_failed": "重命名失败",
"sandbox_root_directory": "根目录", "sandbox_root_directory": "根目录",
"sandbox_save_failed": "保存文件失败",
"sandbox_search_files": "搜索文件", "sandbox_search_files": "搜索文件",
"sandbox_select_file_edit": "选择一个文件进行编辑", "sandbox_select_file_edit": "选择一个文件进行编辑",
"sandbox_save_failed": "保存文件失败", "sandbox_starting": "沙盒启动中(1/2)",
"sandbox_status_checkExisting": "正在检查沙箱环境...",
"sandbox_status_connecting": "正在连接沙箱环境...",
"sandbox_status_creatingContainer": "正在初始化云端沙箱...",
"sandbox_status_deployingSkills": "部署技能 {{skillName}}...", "sandbox_status_deployingSkills": "部署技能 {{skillName}}...",
"sandbox_status_downloadingPackage": "正在下载 Skill 包...",
"sandbox_status_extractingPackage": "正在解压 Skill 包...",
"sandbox_status_failed": "沙箱创建失败",
"sandbox_status_failed_with_message": "沙箱创建失败:{{message}}",
"sandbox_status_fetchSkills": "正在获取技能信息...",
"sandbox_status_lazyInit": "正在准备沙箱环境...",
"sandbox_status_ready_cold": "沙箱环境就绪",
"sandbox_status_ready_warm": "沙箱环境就绪(热启动)",
"sandbox_status_uploadingPackage": "正在上传 Skill 包到沙箱...",
"sandbox_terminal": "终端", "sandbox_terminal": "终端",
"sandbox_upload_failed": "上传失败", "sandbox_upload_failed": "上传失败",
"sandbox_upload_file": "上传文件", "sandbox_upload_file": "上传文件",
...@@ -248,7 +264,6 @@ ...@@ -248,7 +264,6 @@
"setting.save": "保存", "setting.save": "保存",
"setting.save_success": "保存成功", "setting.save_success": "保存成功",
"sidebar.all_apps": "全部应用", "sidebar.all_apps": "全部应用",
"all_apps.title": "应用",
"sidebar.favourite_apps": "精选应用", "sidebar.favourite_apps": "精选应用",
"sidebar.home": "首页", "sidebar.home": "首页",
"sidebar.team_apps": "团队应用", "sidebar.team_apps": "团队应用",
...@@ -259,6 +274,5 @@ ...@@ -259,6 +274,5 @@
"unsupported_file_type": "不支持的文件类型", "unsupported_file_type": "不支持的文件类型",
"variable_invisable_in_share": "外部变量在免登录链接中不可见", "variable_invisable_in_share": "外部变量在免登录链接中不可见",
"view_all_citations": "查看全部", "view_all_citations": "查看全部",
"view_citations": "查看引用", "view_citations": "查看引用"
"run_detail": "运行详情"
} }
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
"permission.only_collaborators": "仅协作者访问", "permission.only_collaborators": "仅协作者访问",
"permission.team_read": "团队可访问", "permission.team_read": "团队可访问",
"permission.team_write": "团队可编辑", "permission.team_write": "团队可编辑",
"promotion.pay": "付费",
"promotion.register": "注册",
"register.confirm": "确认注册", "register.confirm": "确认注册",
"register.register_account": "注册 {{account}} 账号", "register.register_account": "注册 {{account}} 账号",
"register.success": "注册成功", "register.success": "注册成功",
......
{ {
"AI_input_is_empty": "傳送至 AI 節點的內容為空", "AI_input_is_empty": "傳送至 AI 節點的內容為空",
"LLM_model_response_empty": "模型響應為空,請查看模型詳情請求", "LLM_model_response_empty": "模型響應為空,請查看模型詳情請求",
"no_output_content": "應用無輸出內容",
"Next": "下一個", "Next": "下一個",
"Previous": "上一個", "Previous": "上一個",
"agent_plan_generating": "正在生成計畫", "agent_plan_generating": "正在生成計畫",
...@@ -11,6 +10,7 @@ ...@@ -11,6 +10,7 @@
"agent_plan_updating": "正在更新計畫", "agent_plan_updating": "正在更新計畫",
"ai_chat_loading": "AI 對話", "ai_chat_loading": "AI 對話",
"ai_reasoning": "思考過程", "ai_reasoning": "思考過程",
"all_apps.title": "應用",
"back_to_text": "返回輸入", "back_to_text": "返回輸入",
"balance_not_enough_pause": "由於 AI 積分不足,暫停運行工作流", "balance_not_enough_pause": "由於 AI 積分不足,暫停運行工作流",
"chat.quote.No Data": "找不到該文件", "chat.quote.No Data": "找不到該文件",
...@@ -32,23 +32,21 @@ ...@@ -32,23 +32,21 @@
"completion_finish_null": "未知", "completion_finish_null": "未知",
"completion_finish_stop": "正常完成", "completion_finish_stop": "正常完成",
"completion_finish_tool_calls": "工具呼叫", "completion_finish_tool_calls": "工具呼叫",
"feedback_helpful": "有幫助",
"feedback_unhelpful": "沒有幫助",
"compress_llm_messages": "上下文壓縮", "compress_llm_messages": "上下文壓縮",
"confirm_clear_input_value": "確認清空表單內容?\n將會恢復默認值!", "confirm_clear_input_value": "確認清空表單內容?\n將會恢復默認值!",
"mobile_clear_history_confirm_title": "確定清空歷史記錄?",
"mobile_clear_history_confirm_tip": "此操作不可撤銷",
"confirm_to_clear_share_chat_history": "確認清空所有聊天記錄?", "confirm_to_clear_share_chat_history": "確認清空所有聊天記錄?",
"content_empty": "無內容", "content_empty": "無內容",
"continue_run": "繼續運行", "continue_run": "繼續運行",
"dataset_quote_type error": "知識庫引用類型錯誤,正確類型:{ datasetId: string }[]", "dataset_quote_type error": "知識庫引用類型錯誤,正確類型:{ datasetId: string }[]",
"dataset_search": "知識庫檢索", "dataset_search": "知識庫檢索",
"download_chunks": "下載資料", "download_chunks": "下載資料",
"empty_directory": "此目錄中已無項目可選~",
"edit_message": "編輯訊息", "edit_message": "編輯訊息",
"empty_directory": "此目錄中已無項目可選~",
"feedback_helpful": "有幫助",
"feedback_unhelpful": "沒有幫助",
"file_amount_over": "超出檔案數量上限 {{max}}", "file_amount_over": "超出檔案數量上限 {{max}}",
"history_generating": "思考中",
"go_to_dataset": "前往知識庫", "go_to_dataset": "前往知識庫",
"history_generating": "思考中",
"history_slider.home.title": "聊天", "history_slider.home.title": "聊天",
"home.chat_app": "首页聊天", "home.chat_app": "首页聊天",
"home.select_tools": "選擇工具", "home.select_tools": "選擇工具",
...@@ -65,11 +63,16 @@ ...@@ -65,11 +63,16 @@
"log.feedback.read": "已讀", "log.feedback.read": "已讀",
"log.feedback.show_feedback": "顯示反饋", "log.feedback.show_feedback": "顯示反饋",
"master_agent_call": "主 agent 調用", "master_agent_call": "主 agent 調用",
"mobile_clear_history_confirm_tip": "此操作不可撤銷",
"mobile_clear_history_confirm_title": "確定清空歷史記錄?",
"module_runtime_and": "模組執行總時間", "module_runtime_and": "模組執行總時間",
"no_output_content": "應用無輸出內容",
"plan_agent": "規劃 Agent", "plan_agent": "規劃 Agent",
"plan_check_tip": "規劃已生成,您可繼續對話來微調當前規劃", "plan_check_tip": "規劃已生成,您可繼續對話來微調當前規劃",
"plugins_output": "外掛程式輸出", "plugins_output": "外掛程式輸出",
"press_to_speak": "按住說話", "press_to_speak": "按住說話",
"processed": "已處理",
"processing": "處理中",
"quote_result_notice": "僅代表本次搜尋時的結果,請注意資訊的時效性", "quote_result_notice": "僅代表本次搜尋時的結果,請注意資訊的時效性",
"quote_source_title": "引用來源", "quote_source_title": "引用來源",
"read_file": "文件解析", "read_file": "文件解析",
...@@ -148,41 +151,54 @@ ...@@ -148,41 +151,54 @@
"response.update_var_result": "變數更新結果(依序顯示多個變數更新結果)", "response.update_var_result": "變數更新結果(依序顯示多個變數更新結果)",
"response.user_select_result": "使用者選擇結果", "response.user_select_result": "使用者選擇結果",
"resume_placeholder_generating": "生成中", "resume_placeholder_generating": "生成中",
"run_detail": "運行詳情",
"sandbox_audio_not_supported": "目前瀏覽器不支援音訊播放。", "sandbox_audio_not_supported": "目前瀏覽器不支援音訊播放。",
"sandbox_binary_file_no_preview": "無法預覽該文件,請直接下載查看", "sandbox_binary_file_no_preview": "無法預覽該文件,請直接下載查看",
"sandbox_collapse_all": "全部折疊", "sandbox_collapse_all": "全部折疊",
"sandbox_confirm_delete_content": "確定要刪除 <name>{{name}}</name> 嗎?此操作不可撤銷,並將永久刪除所有包含的子文件。", "sandbox_confirm_delete_content": "確定要刪除 <name>{{name}}</name> 嗎?此操作不可撤銷,並將永久刪除所有包含的子文件。",
"sandbox_confirm_delete_title": "確認刪除", "sandbox_confirm_delete_title": "確認刪除",
"sandbox_connect_failed_max_attempts": "連接沙箱失敗次數過多,已停止嘗試。", "sandbox_connect_failed_max_attempts": "連接沙箱失敗次數過多,已停止嘗試。",
"sandbox_refresh_page_to_retry": "請重新整理頁面後重試。",
"sandbox_create_failed": "新建失敗", "sandbox_create_failed": "新建失敗",
"sandbox_delete": "刪除", "sandbox_delete": "刪除",
"sandbox_delete_failed": "刪除失敗", "sandbox_delete_failed": "刪除失敗",
"sandbox_download": "下載", "sandbox_download": "下載",
"sandbox_download_failed": "下載失敗", "sandbox_download_failed": "下載失敗",
"sandbox_entry_tooltip": "查看虛擬機器文件", "sandbox_entry_tooltip": "查看虛擬機器文件",
"sandbox_file_already_exists": "目標目錄中已存在同名文件或資料夾",
"sandbox_file_config": "文件配置", "sandbox_file_config": "文件配置",
"sandbox_file_loading": "文件載入中(2/2)",
"sandbox_files": "虛擬機器文件", "sandbox_files": "虛擬機器文件",
"sandbox_html_preview": "預覽", "sandbox_html_preview": "預覽",
"sandbox_html_preview_failed": "預覽連結產生失敗", "sandbox_html_preview_failed": "預覽連結產生失敗",
"sandbox_load_failed": "載入目錄失敗", "sandbox_load_failed": "載入目錄失敗",
"sandbox_starting": "沙盒啟動中(1/2)",
"sandbox_file_loading": "文件載入中(2/2)",
"sandbox_move_failed": "移動失敗", "sandbox_move_failed": "移動失敗",
"sandbox_move_into_self_forbidden": "不能將目錄移入其自身或其子目錄下", "sandbox_move_into_self_forbidden": "不能將目錄移入其自身或其子目錄下",
"sandbox_file_already_exists": "目標目錄中已存在同名文件或資料夾",
"sandbox_new_file": "新建文件", "sandbox_new_file": "新建文件",
"sandbox_new_file_placeholder": "新建文件...", "sandbox_new_file_placeholder": "新建文件...",
"sandbox_new_folder": "新建資料夾", "sandbox_new_folder": "新建資料夾",
"sandbox_new_folder_placeholder": "新建資料夾...", "sandbox_new_folder_placeholder": "新建資料夾...",
"sandbox_no_file": "虛擬機器裡還沒有文件", "sandbox_no_file": "虛擬機器裡還沒有文件",
"sandbox_refresh_page_to_retry": "請重新整理頁面後重試。",
"sandbox_rename": "重新命名", "sandbox_rename": "重新命名",
"sandbox_rename_failed": "重新命名失敗", "sandbox_rename_failed": "重新命名失敗",
"sandbox_root_directory": "根目錄", "sandbox_root_directory": "根目錄",
"sandbox_save_failed": "儲存文件失敗",
"sandbox_search_files": "搜尋文件", "sandbox_search_files": "搜尋文件",
"sandbox_select_file_edit": "選擇一個文件進行編輯", "sandbox_select_file_edit": "選擇一個文件進行編輯",
"sandbox_save_failed": "儲存文件失敗", "sandbox_starting": "沙盒啟動中(1/2)",
"sandbox_status_checkExisting": "正在檢查沙箱環境...",
"sandbox_status_connecting": "正在連接沙箱環境...",
"sandbox_status_creatingContainer": "正在初始化雲端沙箱...",
"sandbox_status_deployingSkills": "部署技能 {{skillName}}...", "sandbox_status_deployingSkills": "部署技能 {{skillName}}...",
"sandbox_status_downloadingPackage": "正在下載 Skill 包...",
"sandbox_status_extractingPackage": "正在解壓 Skill 包...",
"sandbox_status_failed": "沙箱創建失敗",
"sandbox_status_failed_with_message": "沙箱創建失敗:{{message}}",
"sandbox_status_fetchSkills": "正在獲取技能資訊...",
"sandbox_status_lazyInit": "正在準備沙箱環境...",
"sandbox_status_ready_cold": "沙箱環境就緒",
"sandbox_status_ready_warm": "沙箱環境就緒(熱啟動)",
"sandbox_status_uploadingPackage": "正在上傳 Skill 包到沙箱...",
"sandbox_terminal": "終端機", "sandbox_terminal": "終端機",
"sandbox_upload_failed": "上傳失敗", "sandbox_upload_failed": "上傳失敗",
"sandbox_upload_file": "上傳文件", "sandbox_upload_file": "上傳文件",
...@@ -244,7 +260,6 @@ ...@@ -244,7 +260,6 @@
"setting.save": "保存", "setting.save": "保存",
"setting.save_success": "保存成功", "setting.save_success": "保存成功",
"sidebar.all_apps": "全部應用", "sidebar.all_apps": "全部應用",
"all_apps.title": "應用",
"sidebar.favourite_apps": "精選應用", "sidebar.favourite_apps": "精選應用",
"sidebar.home": "首頁", "sidebar.home": "首頁",
"sidebar.team_apps": "團隊應用", "sidebar.team_apps": "團隊應用",
...@@ -255,6 +270,5 @@ ...@@ -255,6 +270,5 @@
"unsupported_file_type": "不支援的檔案類型", "unsupported_file_type": "不支援的檔案類型",
"variable_invisable_in_share": "外部變量在免登錄鏈接中不可見", "variable_invisable_in_share": "外部變量在免登錄鏈接中不可見",
"view_all_citations": "查看全部", "view_all_citations": "查看全部",
"view_citations": "檢視引用", "view_citations": "檢視引用"
"run_detail": "運行詳情"
} }
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
"permission.only_collaborators": "僅協作者可存取", "permission.only_collaborators": "僅協作者可存取",
"permission.team_read": "團隊可存取", "permission.team_read": "團隊可存取",
"permission.team_write": "團隊可編輯", "permission.team_write": "團隊可編輯",
"promotion.pay": "付費",
"promotion.register": "註冊",
"register.confirm": "確認註冊", "register.confirm": "確認註冊",
"register.register_account": "註冊 {{account}} 帳號", "register.register_account": "註冊 {{account}} 帳號",
"register.success": "註冊成功", "register.success": "註冊成功",
......
...@@ -75,7 +75,7 @@ const LabelAndFormRender = ({ ...@@ -75,7 +75,7 @@ const LabelAndFormRender = ({
if (!hasValue) return false; if (!hasValue) return false;
// 有 minLength 要求且正在输入新值时,检查长度 // 有 minLength 要求且正在输入新值时,检查长度
if (props.minLength && value.value && value.value.length < props.minLength) { if (props.minLength && value.value && value.value.length < props.minLength) {
return t(`common:min_length`, { minLenth: props.minLength }); return t('common:min_length', { minLenth: props.minLength });
} }
return true; return true;
} }
...@@ -93,7 +93,7 @@ const LabelAndFormRender = ({ ...@@ -93,7 +93,7 @@ const LabelAndFormRender = ({
? { ? {
minLength: { minLength: {
value: props.minLength, value: props.minLength,
message: t(`common:min_length`, { minLenth: props.minLength }) message: t('common:min_length', { minLenth: props.minLength })
} }
} }
: {}) : {})
......
...@@ -8,6 +8,7 @@ import { getErrText } from '@fastgpt/global/common/error/utils'; ...@@ -8,6 +8,7 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import type { import type {
AIChatItemValueItemType, AIChatItemValueItemType,
SandboxStatusPhase,
UserChatItemValueItemType UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import { import {
...@@ -204,23 +205,33 @@ export const useChatGenerate = ({ ...@@ -204,23 +205,33 @@ export const useChatGenerate = ({
} }
if (event === SseResponseEventEnum.sandboxStatus && sandboxStatus) { if (event === SseResponseEventEnum.sandboxStatus && sandboxStatus) {
const getSandboxPhaseLabel = (): string => { const getSandboxPhaseLabel = (): string => {
const { phase, isWarmStart, skillName } = sandboxStatus; const { phase, isWarmStart, skillName, message } = sandboxStatus;
if (phase === 'deployingSkills') { const phaseLabelMap: Record<Exclude<SandboxStatusPhase, 'ready' | 'failed'>, string> = {
return t('chat:sandbox_status_deployingSkills', { skillName: skillName ?? '' }); checkExisting: t('chat:sandbox_status_checkExisting'),
} connecting: t('chat:sandbox_status_connecting'),
if ( fetchSkills: t('chat:sandbox_status_fetchSkills'),
phase === 'downloadingPackage' || creatingContainer: t('chat:sandbox_status_creatingContainer'),
phase === 'uploadingPackage' || deployingSkills: t('chat:sandbox_status_deployingSkills', {
phase === 'extractingPackage' skillName: skillName ?? ''
) { }),
return t(`chat:sandbox_status_${phase}` as any, { skillName: skillName ?? '' }); downloadingPackage: t('chat:sandbox_status_downloadingPackage'),
} uploadingPackage: t('chat:sandbox_status_uploadingPackage'),
extractingPackage: t('chat:sandbox_status_extractingPackage'),
lazyInit: t('chat:sandbox_status_lazyInit')
};
if (phase === 'ready') { if (phase === 'ready') {
return t( return isWarmStart
isWarmStart ? 'chat:sandbox_status_ready_warm' : 'chat:sandbox_status_ready_cold' ? t('chat:sandbox_status_ready_warm')
); : t('chat:sandbox_status_ready_cold');
} }
return t(`chat:sandbox_status_${phase}` as any); if (phase === 'failed') {
return message
? t('chat:sandbox_status_failed_with_message', { message })
: t('chat:sandbox_status_failed');
}
return phaseLabelMap[phase];
}; };
return { return {
...item, ...item,
......
...@@ -50,7 +50,7 @@ const RenderProcessingCollapse = React.memo(function RenderProcessingCollapse({ ...@@ -50,7 +50,7 @@ const RenderProcessingCollapse = React.memo(function RenderProcessingCollapse({
> >
<HStack h={'24px'} lineHeight={'24px'} mr={1} spacing="0"> <HStack h={'24px'} lineHeight={'24px'} mr={1} spacing="0">
<Box fontSize={'16px'} lineHeight={'24px'}> <Box fontSize={'16px'} lineHeight={'24px'}>
{t(isProcessing ? 'chat:processing' : 'chat:processed')} {isProcessing ? t('chat:processing') : t('chat:processed')}
{isProcessing && label && ( {isProcessing && label && (
<Box as="span"> <Box as="span">
{' · '} {' · '}
......
...@@ -274,7 +274,9 @@ const EditForm = ({ ...@@ -274,7 +274,9 @@ const EditForm = ({
<SandboxTipTag /> <SandboxTipTag />
) : ( ) : (
<MyTag> <MyTag>
{t(showSandbox ? 'app:sandbox_free_not_support' : 'app:sandbox_not_support_tip')} {showSandbox
? t('app:sandbox_free_not_support')
: t('app:sandbox_not_support_tip')}
</MyTag> </MyTag>
)} )}
</Box> </Box>
......
...@@ -19,7 +19,7 @@ import dynamic from 'next/dynamic'; ...@@ -19,7 +19,7 @@ import dynamic from 'next/dynamic';
import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type'; import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
import { import {
getAppToolTemplates, getAppToolTemplates,
getToolPreviewNode, getClientToolPreviewNode,
getTeamAppTemplates getTeamAppTemplates
} from '@/web/core/app/api/tool'; } from '@/web/core/app/api/tool';
import { import {
...@@ -250,7 +250,7 @@ export const useSkillManager = ({ ...@@ -250,7 +250,7 @@ export const useSkillManager = ({
}; };
} }
const toolTemplate = await getToolPreviewNode({ appId: toolId, versionId: '' }); const toolTemplate = await getClientToolPreviewNode({ appId: toolId, versionId: '' });
const toolValid = validateToolConfiguration({ const toolValid = validateToolConfiguration({
toolTemplate, toolTemplate,
......
...@@ -37,7 +37,7 @@ import { ...@@ -37,7 +37,7 @@ import {
getToolConfigStatus, getToolConfigStatus,
validateToolConfiguration validateToolConfiguration
} from '@fastgpt/global/core/app/formEdit/utils'; } from '@fastgpt/global/core/app/formEdit/utils';
import { getToolPreviewNode } from '@/web/core/app/api/tool'; import { getClientToolPreviewNode } from '@/web/core/app/api/tool';
import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config.schema'; import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config.schema';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
...@@ -431,7 +431,7 @@ export const loadGeneratedTools = async ({ ...@@ -431,7 +431,7 @@ export const loadGeneratedTools = async ({
} }
// 新工具,需要与已配置的 tool 进行 input 合并 // 新工具,需要与已配置的 tool 进行 input 合并
const tool = await getToolPreviewNode({ appId: toolId, versionId: '' }); const tool = await getClientToolPreviewNode({ appId: toolId, versionId: '' });
// 验证工具配置 // 验证工具配置
const toolValid = validateToolConfiguration({ const toolValid = validateToolConfiguration({
toolTemplate: tool, toolTemplate: tool,
......
...@@ -11,7 +11,11 @@ import { ...@@ -11,7 +11,11 @@ import {
type NodeTemplateListItemType type NodeTemplateListItemType
} from '@fastgpt/global/core/workflow/type/node'; } from '@fastgpt/global/core/workflow/type/node';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { getToolPreviewNode, getAppToolTemplates, getAppToolPaths } from '@/web/core/app/api/tool'; import {
getClientToolPreviewNode,
getAppToolTemplates,
getAppToolPaths
} from '@/web/core/app/api/tool';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { getTeamAppTemplates } from '@/web/core/app/api/tool'; import { getTeamAppTemplates } from '@/web/core/app/api/tool';
import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
...@@ -269,7 +273,7 @@ const RenderList = React.memo(function RenderList({ ...@@ -269,7 +273,7 @@ const RenderList = React.memo(function RenderList({
const { runAsync: onClickAdd, loading: isLoading } = useRequest( const { runAsync: onClickAdd, loading: isLoading } = useRequest(
async (template: NodeTemplateListItemType) => { async (template: NodeTemplateListItemType) => {
const res = await getToolPreviewNode({ appId: template.id, versionId: '' }); const res = await getClientToolPreviewNode({ appId: template.id, versionId: '' });
const isToolSetTemplate = template.flowNodeType === FlowNodeTypeEnum.toolSet; const isToolSetTemplate = template.flowNodeType === FlowNodeTypeEnum.toolSet;
if (!isToolSetTemplate) { if (!isToolSetTemplate) {
......
...@@ -103,7 +103,7 @@ const MyEdit = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({ ...@@ -103,7 +103,7 @@ const MyEdit = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({
onClick={async () => { onClick={async () => {
const initialSnapshot = past[past.length - 1]; const initialSnapshot = past[past.length - 1];
onSwitchTmpVersion(initialSnapshot, t(`app:version_initial_copy`)); onSwitchTmpVersion(initialSnapshot, t('app:version_initial_copy'));
toast({ toast({
title: t('workflow:workflow.Switch_success'), title: t('workflow:workflow.Switch_success'),
status: 'success' status: 'success'
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
css css
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { getToolPreviewNode } from '@/web/core/app/api/tool'; import { getClientToolPreviewNode } from '@/web/core/app/api/tool';
import type { import type {
FlowNodeItemType, FlowNodeItemType,
NodeTemplateListItemType, NodeTemplateListItemType,
...@@ -245,7 +245,7 @@ const NodeTemplateList = ({ ...@@ -245,7 +245,7 @@ const NodeTemplateList = ({
].includes(template.flowNodeType); ].includes(template.flowNodeType);
if (shouldLoadPreviewNode) { if (shouldLoadPreviewNode) {
const node = await getToolPreviewNode({ const node = await getClientToolPreviewNode({
appId: template.id, appId: template.id,
getLatestVersion: true getLatestVersion: true
}); });
......
...@@ -548,7 +548,7 @@ export const useWorkflow = () => { ...@@ -548,7 +548,7 @@ export const useWorkflow = () => {
if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) { if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) {
return toast({ return toast({
status: 'warning', status: 'warning',
title: t(isParallel ? 'workflow:can_not_parallel' : 'workflow:can_not_loop') title: isParallel ? t('workflow:can_not_parallel') : t('workflow:can_not_loop')
}); });
} }
......
...@@ -551,7 +551,9 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => { ...@@ -551,7 +551,9 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<SandboxTipTag /> <SandboxTipTag />
) : ( ) : (
<MyTag> <MyTag>
{t(showSandbox ? 'app:sandbox_free_not_support' : 'app:sandbox_not_support_tip')} {showSandbox
? t('app:sandbox_free_not_support')
: t('app:sandbox_not_support_tip')}
</MyTag> </MyTag>
)} )}
<Switch <Switch
......
...@@ -22,7 +22,7 @@ import { LOGO_ICON } from '@fastgpt/global/common/system/constants'; ...@@ -22,7 +22,7 @@ import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { ToolSourceHandle, ToolTargetHandle } from './Handle/ToolHandle'; import { ToolSourceHandle, ToolTargetHandle } from './Handle/ToolHandle';
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle'; import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
import { useDebug } from '../../hooks/useDebug'; import { useDebug } from '../../hooks/useDebug';
import { getToolPreviewNode } from '@/web/core/app/api/tool'; import { getClientToolPreviewNode } from '@/web/core/app/api/tool';
import { getAppVersionList } from '@/web/core/app/api/version'; import { getAppVersionList } from '@/web/core/app/api/version';
import { getTeamToolVersions } from '@/web/core/plugin/team/api'; import { getTeamToolVersions } from '@/web/core/plugin/team/api';
import { storeNode2FlowNode } from '@/web/core/workflow/utils'; import { storeNode2FlowNode } from '@/web/core/workflow/utils';
...@@ -655,7 +655,7 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt ...@@ -655,7 +655,7 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
if (!node) return; if (!node) return;
if (node.pluginId) { if (node.pluginId) {
const template = await getToolPreviewNode({ const template = await getClientToolPreviewNode({
appId: node.pluginId, appId: node.pluginId,
versionId versionId
}); });
......
...@@ -286,7 +286,7 @@ export const WorkflowUtilsProvider = ({ children }: { children: ReactNode }) => ...@@ -286,7 +286,7 @@ export const WorkflowUtilsProvider = ({ children }: { children: ReactNode }) =>
{ {
nodes: nodes, nodes: nodes,
edges: edges, edges: edges,
title: t(`app:app.version_initial`), title: t('app:app.version_initial'),
isSaved: true, isSaved: true,
chatConfig: e.chatConfig || appDetail.chatConfig chatConfig: e.chatConfig || appDetail.chatConfig
} }
......
import { import { Box, Button, Flex, HStack, Input, useDisclosure } from '@chakra-ui/react';
Box,
Button,
Flex,
HStack,
Input,
ModalBody,
ModalFooter,
useDisclosure
} from '@chakra-ui/react';
import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio'; import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio';
...@@ -19,7 +10,7 @@ import type { FlowNodeInputItemType, InputConfigType } from '@fastgpt/global/cor ...@@ -19,7 +10,7 @@ import type { FlowNodeInputItemType, InputConfigType } from '@fastgpt/global/cor
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import IconButton from '@/pageComponents/account/team/OrgManage/IconButton'; import IconButton from '@/pageComponents/account/team/OrgManage/IconButton';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/v2/common/MyModal';
import InputRender from '@/components/core/app/formRender'; import InputRender from '@/components/core/app/formRender';
import { secretInputTypeToInputType } from '@/components/core/app/formRender/utils'; import { secretInputTypeToInputType } from '@/components/core/app/formRender/utils';
import { getAppToolTemplates } from '@/web/core/app/api/tool'; import { getAppToolTemplates } from '@/web/core/app/api/tool';
...@@ -105,13 +96,22 @@ const SecretInputModal = ({ ...@@ -105,13 +96,22 @@ const SecretInputModal = ({
return ( return (
<MyModal <MyModal
isOpen isOpen
iconSrc={'key'}
iconColor={'primary.600'}
title={t('workflow:tool_active_config')} title={t('workflow:tool_active_config')}
onClose={onClose} onClose={onClose}
w={'500px'} size={'md'}
isCentered
footer={
<>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common:Cancel')}
</Button>
<Button variant={'primary'} onClick={handleSubmit(onSubmit)}>
{t('common:Confirm')}
</Button>
</>
}
> >
<ModalBody pt={6}> <>
<FormLabel mb={1} fontSize={'md'}> <FormLabel mb={1} fontSize={'md'}>
{t('common:secret_key')} {t('common:secret_key')}
</FormLabel> </FormLabel>
...@@ -228,12 +228,13 @@ const SecretInputModal = ({ ...@@ -228,12 +228,13 @@ const SecretInputModal = ({
const inputKey = `value.${item.key}.value` as any; const inputKey = `value.${item.key}.value` as any;
const value = getValues(`value.${item.key}`); const value = getValues(`value.${item.key}`);
const showInput = !!value?.value || !value?.secret || editIndex === i; const showInput = !!value?.value || !value?.secret || editIndex === i;
const fieldLabel = t(item.label as any);
return ( return (
<Box key={item.key} mb={inputList.length - 1 === i ? 2 : 5}> <Box key={item.key} mb={inputList.length - 1 === i ? 2 : 5}>
<Flex alignItems={'center'} mb={0.5}> <Flex alignItems={'center'} mb={0.5}>
<FormLabel required={item.required} color={'myGray.600'}> <FormLabel required={item.required} color={'myGray.600'}>
{t(item.label as any)} {fieldLabel}
</FormLabel> </FormLabel>
{item.description && <QuestionTip label={item.description} />} {item.description && <QuestionTip label={item.description} />}
<Box flex={'1 0 0'} /> <Box flex={'1 0 0'} />
...@@ -243,7 +244,13 @@ const SecretInputModal = ({ ...@@ -243,7 +244,13 @@ const SecretInputModal = ({
{showInput ? ( {showInput ? (
<Input <Input
bg={'myGray.50'} bg={'myGray.50'}
placeholder={item.description} h={'48px'}
py={0}
display={'flex'}
alignItems={'center'}
lineHeight={'normal'}
_placeholder={{ lineHeight: 'normal' }}
placeholder={fieldLabel}
{...register(inputKey, { {...register(inputKey, {
required: item.required required: item.required
})} })}
...@@ -275,6 +282,27 @@ const SecretInputModal = ({ ...@@ -275,6 +282,27 @@ const SecretInputModal = ({
</> </>
)} )}
</Flex> </Flex>
) : item.inputType === 'input' ? (
<Controller
control={control}
name={inputKey}
rules={{
required: item.required
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<Input
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
bg={'myGray.50'}
h={'48px'}
py={0}
lineHeight={'normal'}
_placeholder={{ lineHeight: 'normal' }}
placeholder={fieldLabel}
isInvalid={!!error}
/>
)}
/>
) : ( ) : (
<Controller <Controller
control={control} control={control}
...@@ -299,7 +327,7 @@ const SecretInputModal = ({ ...@@ -299,7 +327,7 @@ const SecretInputModal = ({
inputType={secretInputTypeToInputType(item.inputType)} inputType={secretInputTypeToInputType(item.inputType)}
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder={item.description} placeholder={fieldLabel}
bg={'myGray.50'} bg={'myGray.50'}
list={item.list} list={item.list}
isInvalid={!!error} isInvalid={!!error}
...@@ -319,15 +347,7 @@ const SecretInputModal = ({ ...@@ -319,15 +347,7 @@ const SecretInputModal = ({
onChange={(e) => setValue('type', e)} onChange={(e) => setValue('type', e)}
/> />
</Box> </Box>
</ModalBody> </>
<ModalFooter>
<Button mr={4} variant={'whiteBase'} onClick={onClose}>
{t('common:Cancel')}
</Button>
<Button variant={'primary'} onClick={handleSubmit(onSubmit)}>
{t('common:Confirm')}
</Button>
</ModalFooter>
</MyModal> </MyModal>
); );
}; };
......
...@@ -32,7 +32,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; ...@@ -32,7 +32,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import ChatAIModelSelector from './ChatAIModelSelector'; import ChatAIModelSelector from './ChatAIModelSelector';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils'; import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import { getToolPreviewNode } from '@/web/core/app/api/tool'; import { getClientToolPreviewNode } from '@/web/core/app/api/tool';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { getWebLLMModel } from '@/web/common/system/utils'; import { getWebLLMModel } from '@/web/common/system/utils';
import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { ChatPageContext } from '@/web/core/chat/context/chatPageContext';
...@@ -247,7 +247,7 @@ const HomeChatWindow = () => { ...@@ -247,7 +247,7 @@ const HomeChatWindow = () => {
const tools: FlowNodeTemplateType[] = await Promise.all( const tools: FlowNodeTemplateType[] = await Promise.all(
selectedToolIds.map(async (toolId) => { selectedToolIds.map(async (toolId) => {
const node = await getToolPreviewNode({ appId: toolId, versionId: '' }); const node = await getClientToolPreviewNode({ appId: toolId, versionId: '' });
node.inputs = node.inputs.map((input) => { node.inputs = node.inputs.map((input) => {
const tool = availableTools.find((tool) => tool.pluginId === toolId); const tool = availableTools.find((tool) => tool.pluginId === toolId);
const value = tool?.inputs?.[input.key]; const value = tool?.inputs?.[input.key];
......
...@@ -52,6 +52,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast'; ...@@ -52,6 +52,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import type { UpdateSystemToolBodyType } from '@fastgpt/global/openapi/core/plugin/admin/tool/api'; import type { UpdateSystemToolBodyType } from '@fastgpt/global/openapi/core/plugin/admin/tool/api';
import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { jsonSchema2SecretInput } from '@fastgpt/global/core/app/jsonschema';
const COST_LIMITS = { max: 1000, min: 0, step: 0.1 }; const COST_LIMITS = { max: 1000, min: 0, step: 0.1 };
const FORM_LABEL_WIDTH = '160px'; const FORM_LABEL_WIDTH = '160px';
...@@ -246,7 +247,10 @@ const SystemToolConfigModal = ({ ...@@ -246,7 +247,10 @@ const SystemToolConfigModal = ({
]); ]);
// 从 tool 读取只读数据 // 从 tool 读取只读数据
const inputList = tool?.secrets; const inputList = useMemo(
() => jsonSchema2SecretInput({ jsonSchema: tool?.secretSchema }) ?? [],
[tool?.secretSchema]
);
const isFolder = tool?.isToolSet; const isFolder = tool?.isToolSet;
const modalWidth = isFolder ? TOOL_SET_MODAL_WIDTH : SINGLE_TOOL_MODAL_WIDTH; const modalWidth = isFolder ? TOOL_SET_MODAL_WIDTH : SINGLE_TOOL_MODAL_WIDTH;
...@@ -381,6 +385,7 @@ const SystemToolConfigModal = ({ ...@@ -381,6 +385,7 @@ const SystemToolConfigModal = ({
// Secret input render // Secret input render
const renderInputField = (item: InputConfigType) => { const renderInputField = (item: InputConfigType) => {
const fieldValue = secretsVal?.[item.key];
const labelSection = ( const labelSection = (
<HStack> <HStack>
<Box position={'relative'} fontSize={'14px'} fontWeight={'500'}> <Box position={'relative'} fontSize={'14px'} fontWeight={'500'}>
...@@ -403,6 +408,26 @@ const SystemToolConfigModal = ({ ...@@ -403,6 +408,26 @@ const SystemToolConfigModal = ({
); );
} }
if (item.inputType === 'select') {
return (
<ConfigRow key={item.key} label={labelSection}>
<MySelect<string>
bg={'white'}
h={9}
list={item.list ?? []}
value={typeof fieldValue === 'string' ? fieldValue : undefined}
placeholder={item.label}
onChange={(value) => {
setValue(`secretsVal.${item.key}`, value, {
shouldDirty: true,
shouldValidate: true
});
}}
/>
</ConfigRow>
);
}
return ( return (
<ConfigRow key={item.key} label={labelSection}> <ConfigRow key={item.key} label={labelSection}>
<Input <Input
......
...@@ -119,7 +119,11 @@ const Promotion = () => { ...@@ -119,7 +119,11 @@ const Promotion = () => {
<Td> <Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'} {item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td> </Td>
<Td>{t(`user:promotion.${item.type}` as any)}</Td> <Td>
{item.type === 'register'
? t('user:promotion.register')
: t('user:promotion.pay')}
</Td>
<Td>{item.amount}</Td> <Td>{item.amount}</Td>
</Tr> </Tr>
))} ))}
......
/* /*
get plugin preview modules get plugin preview modules
*/ */
import { getChildAppPreviewNode } from '@fastgpt/service/core/app/tool/controller'; import { getClientToolPreviewNode } from '@fastgpt/service/core/app/tool/utils/client';
import { type FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; import { type FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { type ApiRequestProps } from '@fastgpt/service/type/next';
...@@ -32,7 +32,7 @@ async function handler( ...@@ -32,7 +32,7 @@ async function handler(
} }
return GetPreviewNodeResponseSchema.parse( return GetPreviewNodeResponseSchema.parse(
await getChildAppPreviewNode({ await getClientToolPreviewNode({
appId, appId,
versionId, versionId,
getLatestVersion, getLatestVersion,
......
...@@ -37,7 +37,7 @@ export async function handler( ...@@ -37,7 +37,7 @@ export async function handler(
// const tools = await getSystemToolsWithInstalled({ teamId, isRoot, userTags }); // const tools = await getSystemToolsWithInstalled({ teamId, isRoot, userTags });
const systemToolRepo = SystemToolRepo.getInstance(); const systemToolRepo = SystemToolRepo.getInstance();
if (parentId) { if (parentId) {
const parent = await systemToolRepo.getSystemToolDetail({ const parent = await systemToolRepo.getSystemToolDisplayInfo({
pluginId: parentId, pluginId: parentId,
lang, lang,
source: 'system' source: 'system'
......
...@@ -68,7 +68,7 @@ async function getToolPathItem({ ...@@ -68,7 +68,7 @@ async function getToolPathItem({
}): Promise<GetToolPathResponseType[number]> { }): Promise<GetToolPathResponseType[number]> {
const systemToolRepo = SystemToolRepo.getInstance(); const systemToolRepo = SystemToolRepo.getInstance();
const { source } = splitCombineToolId(toolId); const { source } = splitCombineToolId(toolId);
const tool = await systemToolRepo.getSystemToolDetail({ const tool = await systemToolRepo.getSystemToolDisplayInfo({
pluginId: toolId, pluginId: toolId,
lang, lang,
source: source === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : 'system' source: source === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : 'system'
......
...@@ -93,5 +93,5 @@ export const getAppToolPaths = (data: GetPathProps) => { ...@@ -93,5 +93,5 @@ export const getAppToolPaths = (data: GetPathProps) => {
return GET<ParentTreePathItemType[]>('/core/app/tool/path', data); return GET<ParentTreePathItemType[]>('/core/app/tool/path', data);
}; };
export const getToolPreviewNode = (data: GetPreviewNodeQuery) => export const getClientToolPreviewNode = (data: GetPreviewNodeQuery) =>
GET<FlowNodeTemplateType>('/core/app/tool/getPreviewNode', data); GET<FlowNodeTemplateType>('/core/app/tool/getPreviewNode', data);
...@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({ ...@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
getUserDetail: vi.fn(), getUserDetail: vi.fn(),
getSystemToolList: vi.fn(), getSystemToolList: vi.fn(),
getSystemToolDetail: vi.fn(), getSystemToolDetail: vi.fn(),
getSystemToolDisplayInfo: vi.fn(),
getInstance: vi.fn() getInstance: vi.fn()
})); }));
...@@ -45,7 +46,8 @@ describe('get system tool templates handler', () => { ...@@ -45,7 +46,8 @@ describe('get system tool templates handler', () => {
mocks.getUserDetail.mockResolvedValue({ tags: ['hidden-user'] }); mocks.getUserDetail.mockResolvedValue({ tags: ['hidden-user'] });
mocks.getInstance.mockReturnValue({ mocks.getInstance.mockReturnValue({
getSystemToolList: mocks.getSystemToolList, getSystemToolList: mocks.getSystemToolList,
getSystemToolDetail: mocks.getSystemToolDetail getSystemToolDetail: mocks.getSystemToolDetail,
getSystemToolDisplayInfo: mocks.getSystemToolDisplayInfo
}); });
}); });
...@@ -94,7 +96,7 @@ describe('get system tool templates handler', () => { ...@@ -94,7 +96,7 @@ describe('get system tool templates handler', () => {
}); });
it('filters toolset children by escaped searchKey', async () => { it('filters toolset children by escaped searchKey', async () => {
mocks.getSystemToolDetail.mockResolvedValue({ mocks.getSystemToolDisplayInfo.mockResolvedValue({
id: 'toolset', id: 'toolset',
name: 'Toolset', name: 'Toolset',
intro: 'Parent intro', intro: 'Parent intro',
...@@ -133,10 +135,11 @@ describe('get system tool templates handler', () => { ...@@ -133,10 +135,11 @@ describe('get system tool templates handler', () => {
systemKeyCost: 0.5, systemKeyCost: 0.5,
hasTokenFee: true hasTokenFee: true
}); });
expect(mocks.getSystemToolDetail).toHaveBeenCalledWith({ expect(mocks.getSystemToolDisplayInfo).toHaveBeenCalledWith({
pluginId: 'toolset', pluginId: 'toolset',
lang: 'zh', lang: 'zh',
source: 'system' source: 'system'
}); });
expect(mocks.getSystemToolDetail).not.toHaveBeenCalled();
}); });
}); });
...@@ -4,6 +4,7 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/nex ...@@ -4,6 +4,7 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/nex
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
getLocale: vi.fn(), getLocale: vi.fn(),
getSystemToolDetail: vi.fn(), getSystemToolDetail: vi.fn(),
getSystemToolDisplayInfo: vi.fn(),
getInstance: vi.fn() getInstance: vi.fn()
})); }));
...@@ -24,22 +25,23 @@ describe('system tool path handler', () => { ...@@ -24,22 +25,23 @@ describe('system tool path handler', () => {
vi.clearAllMocks(); vi.clearAllMocks();
mocks.getLocale.mockReturnValue('en'); mocks.getLocale.mockReturnValue('en');
mocks.getInstance.mockReturnValue({ mocks.getInstance.mockReturnValue({
getSystemToolDetail: mocks.getSystemToolDetail getSystemToolDetail: mocks.getSystemToolDetail,
getSystemToolDisplayInfo: mocks.getSystemToolDisplayInfo
}); });
}); });
it('returns empty paths when sourceId is empty', async () => { it('returns empty paths when sourceId is empty', async () => {
const result = await handler( const result = await handler(
{ query: {} } as ApiRequestProps<{}, { sourceId?: string }>, { query: {} } as ApiRequestProps<Record<string, never>, { sourceId?: string }>,
{} as ApiResponseType<any> {} as ApiResponseType<any>
); );
expect(result).toEqual([]); expect(result).toEqual([]);
expect(mocks.getSystemToolDetail).not.toHaveBeenCalled(); expect(mocks.getSystemToolDisplayInfo).not.toHaveBeenCalled();
}); });
it('returns current top-level system tool path', async () => { it('returns current top-level system tool path', async () => {
mocks.getSystemToolDetail.mockResolvedValueOnce({ mocks.getSystemToolDisplayInfo.mockResolvedValueOnce({
name: 'Weather' name: 'Weather'
}); });
...@@ -49,7 +51,7 @@ describe('system tool path handler', () => { ...@@ -49,7 +51,7 @@ describe('system tool path handler', () => {
sourceId: 'systemTool-weather', sourceId: 'systemTool-weather',
type: 'current' type: 'current'
} }
} as ApiRequestProps<{}, { sourceId: string; type: 'current' }>, } as ApiRequestProps<Record<string, never>, { sourceId: string; type: 'current' }>,
{} as ApiResponseType<any> {} as ApiResponseType<any>
); );
...@@ -59,15 +61,16 @@ describe('system tool path handler', () => { ...@@ -59,15 +61,16 @@ describe('system tool path handler', () => {
parentName: 'Weather' parentName: 'Weather'
} }
]); ]);
expect(mocks.getSystemToolDetail).toHaveBeenCalledWith({ expect(mocks.getSystemToolDisplayInfo).toHaveBeenCalledWith({
pluginId: 'systemTool-weather', pluginId: 'systemTool-weather',
lang: 'en', lang: 'en',
source: 'system' source: 'system'
}); });
expect(mocks.getSystemToolDetail).not.toHaveBeenCalled();
}); });
it('returns parent and child paths for system toolset child', async () => { it('returns parent and child paths for system toolset child', async () => {
mocks.getSystemToolDetail.mockImplementation(({ pluginId }) => mocks.getSystemToolDisplayInfo.mockImplementation(({ pluginId }) =>
Promise.resolve({ Promise.resolve({
name: pluginId === 'systemTool-map' ? 'Map' : 'Geocode' name: pluginId === 'systemTool-map' ? 'Map' : 'Geocode'
}) })
...@@ -79,7 +82,7 @@ describe('system tool path handler', () => { ...@@ -79,7 +82,7 @@ describe('system tool path handler', () => {
sourceId: 'systemTool-map/geocode', sourceId: 'systemTool-map/geocode',
type: 'current' type: 'current'
} }
} as ApiRequestProps<{}, { sourceId: string; type: 'current' }>, } as ApiRequestProps<Record<string, never>, { sourceId: string; type: 'current' }>,
{} as ApiResponseType<any> {} as ApiResponseType<any>
); );
...@@ -96,7 +99,7 @@ describe('system tool path handler', () => { ...@@ -96,7 +99,7 @@ describe('system tool path handler', () => {
}); });
it('returns only parent path when querying parent of system toolset child', async () => { it('returns only parent path when querying parent of system toolset child', async () => {
mocks.getSystemToolDetail.mockResolvedValueOnce({ mocks.getSystemToolDisplayInfo.mockResolvedValueOnce({
name: 'Map' name: 'Map'
}); });
...@@ -106,7 +109,7 @@ describe('system tool path handler', () => { ...@@ -106,7 +109,7 @@ describe('system tool path handler', () => {
sourceId: 'systemTool-map/geocode', sourceId: 'systemTool-map/geocode',
type: 'parent' type: 'parent'
} }
} as ApiRequestProps<{}, { sourceId: string; type: 'parent' }>, } as ApiRequestProps<Record<string, never>, { sourceId: string; type: 'parent' }>,
{} as ApiResponseType<any> {} as ApiResponseType<any>
); );
...@@ -116,6 +119,7 @@ describe('system tool path handler', () => { ...@@ -116,6 +119,7 @@ describe('system tool path handler', () => {
parentName: 'Map' parentName: 'Map'
} }
]); ]);
expect(mocks.getSystemToolDetail).toHaveBeenCalledTimes(1); expect(mocks.getSystemToolDisplayInfo).toHaveBeenCalledTimes(1);
expect(mocks.getSystemToolDetail).not.toHaveBeenCalled();
}); });
}); });
...@@ -18,16 +18,16 @@ import type { ...@@ -18,16 +18,16 @@ import type {
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { getUser } from '@test/datas/users'; import { getUser } from '@test/datas/users';
const { getChildAppPreviewNodeMock, authAppByTmbIdMock } = vi.hoisted(() => ({ const { getClientToolPreviewNodeMock, authAppByTmbIdMock } = vi.hoisted(() => ({
getChildAppPreviewNodeMock: vi.fn(), getClientToolPreviewNodeMock: vi.fn(),
authAppByTmbIdMock: vi.fn() authAppByTmbIdMock: vi.fn()
})); }));
vi.mock('@fastgpt/service/core/app/tool/controller', async (importOriginal) => { vi.mock('@fastgpt/service/core/app/tool/utils/client', async (importOriginal) => {
const mod = await importOriginal<typeof import('@fastgpt/service/core/app/tool/controller')>(); const mod = await importOriginal<typeof import('@fastgpt/service/core/app/tool/utils/client')>();
return { return {
...mod, ...mod,
getChildAppPreviewNode: getChildAppPreviewNodeMock getClientToolPreviewNode: getClientToolPreviewNodeMock
}; };
}); });
...@@ -43,7 +43,7 @@ const { rewriteAppWorkflowToDetail } = await import('@fastgpt/service/core/app/u ...@@ -43,7 +43,7 @@ const { rewriteAppWorkflowToDetail } = await import('@fastgpt/service/core/app/u
describe('rewriteAppWorkflowToDetail - agent skills', () => { describe('rewriteAppWorkflowToDetail - agent skills', () => {
beforeEach(() => { beforeEach(() => {
getChildAppPreviewNodeMock.mockReset(); getClientToolPreviewNodeMock.mockReset();
authAppByTmbIdMock.mockReset(); authAppByTmbIdMock.mockReset();
}); });
...@@ -126,7 +126,7 @@ describe('rewriteAppWorkflowToDetail - agent skills', () => { ...@@ -126,7 +126,7 @@ describe('rewriteAppWorkflowToDetail - agent skills', () => {
}); });
it('刷新最新工具节点时使用新 renderTypeList,并保留旧节点选中的引用类型', async () => { it('刷新最新工具节点时使用新 renderTypeList,并保留旧节点选中的引用类型', async () => {
getChildAppPreviewNodeMock.mockResolvedValue({ getClientToolPreviewNodeMock.mockResolvedValue({
id: 'mcp-app-1/tool', id: 'mcp-app-1/tool',
flowNodeType: FlowNodeTypeEnum.tool, flowNodeType: FlowNodeTypeEnum.tool,
name: 'Tool', name: 'Tool',
...@@ -230,12 +230,12 @@ describe('rewriteAppWorkflowToDetail - agent skills', () => { ...@@ -230,12 +230,12 @@ describe('rewriteAppWorkflowToDetail - agent skills', () => {
expect(toolInput.value).toEqual(toolReferenceValue); expect(toolInput.value).toEqual(toolReferenceValue);
expect(skillsInput.value).toEqual(skillReferenceValue); expect(skillsInput.value).toEqual(skillReferenceValue);
expect(getChildAppPreviewNodeMock).not.toHaveBeenCalled(); expect(getClientToolPreviewNodeMock).not.toHaveBeenCalled();
}); });
it('校验嵌套工具权限时透传 root 身份', async () => { it('校验嵌套工具权限时透传 root 身份', async () => {
const toolAppId = '507f1f77bcf86cd799439011'; const toolAppId = '507f1f77bcf86cd799439011';
getChildAppPreviewNodeMock.mockResolvedValue({ getClientToolPreviewNodeMock.mockResolvedValue({
id: toolAppId, id: toolAppId,
flowNodeType: FlowNodeTypeEnum.tool, flowNodeType: FlowNodeTypeEnum.tool,
name: 'Personal Tool', name: 'Personal Tool',
......
...@@ -23,6 +23,9 @@ export type ToolListResponse = PaginationResponse<ToolListItem>; ...@@ -23,6 +23,9 @@ export type ToolListResponse = PaginationResponse<ToolListItem>;
const getToolTags = (item: { tags?: readonly string[] | null }): readonly string[] => const getToolTags = (item: { tags?: readonly string[] | null }): readonly string[] =>
Array.isArray(item.tags) ? item.tags : []; Array.isArray(item.tags) ? item.tags : [];
const hasSecretSchemaProperties = (secretSchema?: { properties?: Record<string, unknown> }) =>
!!secretSchema?.properties && Object.keys(secretSchema.properties).length > 0;
async function handler( async function handler(
req: ApiRequestProps<ToolListBody, ToolListQuery>, req: ApiRequestProps<ToolListBody, ToolListQuery>,
res: ApiResponseType<any> res: ApiResponseType<any>
...@@ -51,7 +54,7 @@ async function handler( ...@@ -51,7 +54,7 @@ async function handler(
return { return {
list: filteredData.slice(offset, offset + pageSize).map((item) => ({ list: filteredData.slice(offset, offset + pageSize).map((item) => ({
...item, ...item,
hasSecret: !!item?.secretSchema?.properties?.length, hasSecret: hasSecretSchemaProperties(item.secretSchema),
downloadCount: item.downloadCount, downloadCount: item.downloadCount,
downloadUrl: item.downloadUrl || getPkgdownloadURL(item.toolId) downloadUrl: item.downloadUrl || getPkgdownloadURL(item.toolId)
})), })),
......
...@@ -104,7 +104,7 @@ describe('/api/tool/list', () => { ...@@ -104,7 +104,7 @@ describe('/api/tool/list', () => {
id: 'missing-tags', id: 'missing-tags',
pluginId: 'missing-tags', pluginId: 'missing-tags',
tags: undefined, tags: undefined,
secretSchema: { properties: [{ key: 'apiKey' }] } secretSchema: { properties: { apiKey: { type: 'string' } } }
}) })
]); ]);
......
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