Commit 9bb91d9b by Finley Ge Committed by GitHub

refactor: plugin (#7049)

* fix: workflow system tool use pluginModule

* wip: refactor plugin

* wip: refactor plugin

* wip: refactor plugin

* wip: refactor plugin

* wip: refactor plugin

* chore: split tool detail / runtime config

* fix: workflow system tool use pluginModule

* chore(marketplace): download pkg filename

* chore(marketplace): tc

* chore(marketplace): source

* Use pluginClient fallbackLatestVersion directly

* fix: secret jsonschema to node secret input with enum optionj

* fix: toolset expand logic

* fix: tag filter & ui right arrow if the tool is toolset

* fix: tag filter & ui right arrow if the tool is toolse

* Support null system secrets in tool config

* Clean up workflow tool associations and enforce upload limits

* Adapt runtime config UI to fastgpt-plugin sdk v0.0.1-alpha.6

* Add admin tool search and schema updates

* Fix workflow tool modal and runtime error handling

* Add system secret status to admin tool list

* fix: debug input

* Handle missing marketplace tags in tool list

* Refine workflow tool config labels and layout

* fix: workflow tool config

* fix: wecom corp token

* fix: workflow system tool use pluginModule

* fix: workflow system tool use pluginModule

* fix: wecom corp token get

* fix: workflow system tool use pluginModule

* docs: update v4.15.0-beta-4

* Fix system toolset runtime node selection and cache tests

* Mock plugin upload file reads in tests

* fix: typecheck

* fix: typecheck

* fix: some bug

* fix: sub tool versiosn

* fix: agent v2 invoke workflow tool
parent 04c50f71
...@@ -158,7 +158,9 @@ const loadArgs = (version) => { ...@@ -158,7 +158,9 @@ const loadArgs = (version) => {
/** /**
* @type {{tags: Record<Services, string>, images: Record<Services, Record<string, string>>}} * @type {{tags: Record<Services, string>, images: Record<Services, Record<string, string>>}}
*/ */
const obj = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'version', version, 'args.json'))); const obj = JSON.parse(
fs.readFileSync(path.join(process.cwd(), 'version', version, 'args.json'))
);
const args = {}; const args = {};
for (const key of Object.keys(obj.tags)) { for (const key of Object.keys(obj.tags)) {
args[key] = { args[key] = {
...@@ -232,7 +234,7 @@ const generateDevFile = async (deployVersions, vectors) => { ...@@ -232,7 +234,7 @@ const generateDevFile = async (deployVersions, vectors) => {
) )
]); ]);
console.log('success geenrate dev files'); console.log('success generated dev files');
}; };
/** /**
...@@ -244,7 +246,9 @@ const generateProdFile = async (deployVersions, vectors) => { ...@@ -244,7 +246,9 @@ const generateProdFile = async (deployVersions, vectors) => {
console.log('generating public prod docker-compose.yml files'); console.log('generating public prod docker-compose.yml files');
const outputRoot = path.join(process.cwd(), '..', 'document', 'public', 'deploy', 'docker'); const outputRoot = path.join(process.cwd(), '..', 'document', 'public', 'deploy', 'docker');
const regions = Object.values(RegionEnum); const regions = Object.values(RegionEnum);
const versionArgs = Object.fromEntries(deployVersions.map((version) => [version, loadArgs(version)])); const versionArgs = Object.fromEntries(
deployVersions.map((version) => [version, loadArgs(version)])
);
const versionTemplates = Object.fromEntries( const versionTemplates = Object.fromEntries(
await Promise.all( await Promise.all(
deployVersions.map(async (version) => [ deployVersions.map(async (version) => [
...@@ -272,17 +276,22 @@ const generateProdFile = async (deployVersions, vectors) => { ...@@ -272,17 +276,22 @@ const generateProdFile = async (deployVersions, vectors) => {
Object.entries(vectors).map(([vector, { filename }]) => Object.entries(vectors).map(([vector, { filename }]) =>
fs.promises.writeFile( fs.promises.writeFile(
path.join(outputRoot, version, region, `docker-compose.${filename}.yml`), path.join(outputRoot, version, region, `docker-compose.${filename}.yml`),
formatYamlOutput(replace(versionTemplates[version], region, vector, versionArgs[version], vectors)) formatYamlOutput(
replace(versionTemplates[version], region, vector, versionArgs[version], vectors)
)
) )
) )
) )
) )
); );
console.log('success geenrate prod files'); console.log('success generated prod files');
}; };
const deployVersions = await loadDeployVersions(); const deployVersions = await loadDeployVersions();
await syncInstallScriptVersions(deployVersions); await syncInstallScriptVersions(deployVersions);
const vectors = await loadVectorConfigs(); const vectors = await loadVectorConfigs();
await Promise.all([generateDevFile(deployVersions, vectors), generateProdFile(deployVersions, vectors)]); await Promise.all([
generateDevFile(deployVersions, vectors),
generateProdFile(deployVersions, vectors)
]);
...@@ -3,17 +3,53 @@ title: 'V4.15.0-beta4(进行中)' ...@@ -3,17 +3,53 @@ title: 'V4.15.0-beta4(进行中)'
description: 'FastGPT V4.15.0-beta4 更新说明' description: 'FastGPT V4.15.0-beta4 更新说明'
--- ---
## 📦 升级指南
‼️重要更新,插件服务更新到 v1.0.0-alpha1 版本,系统工具运行方式有较大调整。
### 镜像变更
- 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.15.0-beta4
- 更新 fastgpt-pro(fastgpt 商业版) 镜像 tag: v4.15.0-beta4
- 更新 fastgpt-plugin 镜像 tag: v1.0.0-alpha1
### 插件服务升级
本次插件服务将系统工具从旧的内置工具源码与运行时缓存机制,升级为独立插件包的上传、安装、版本管理和 local-pool 运行机制。
- 升级后需要使用官方系统工具 zip 包重新导入系统工具。
- 模型配置和 workflow 模板保持兼容,无需专项迁移。
- 插件服务建议连接新的 MongoDB 数据库,保留旧插件服务数据库和对象存储数据,便于回滚和审计。
- 插件服务生产环境需设置强 `AUTH_TOKEN`,避免继续使用默认 token。
插件服务可继续复用原 MongoDB 实例,但需要修改数据库名,让新版插件数据写入新库。参考环境变量如下:
```yaml
fastgpt-plugin:
environment:
MONGODB_URI: mongodb://myusername:mypassword@fastgpt-mongo:27017/fastgpt-plugin-v1?authSource=admin
```
## 🚀 新增内容 ## 🚀 新增内容
1. 应用/知识库增加虚拟列表渲染。 1. 系统工具支持以独立 `.pkg` 插件包方式安装。
2. 增加单独的 openapi 文档,区分 devapi 文档。 2. 系统工具支持上传官方 zip 包批量导入。
3. 插件服务支持插件上传确认、URL 安装、版本列表、删除和 disabled 清理。
4. 插件版本支持通过 `pluginId`、`version` 和 `etag` 唯一识别。
5. 插件服务启动时会自动注册 active 插件到 local-pool 运行时。
6. 应用/知识库增加虚拟列表渲染。
7. 增加单独的 openapi 文档,区分 devapi 文档。
## ⚙️ 优化 ## ⚙️ 优化
1. 输入引导配置增加校验,避免错误配置了自定义词库地址。 1. 系统工具运行迁移到 local-pool,支持进程池、队列、超时、重试退避和运行指标。
2. 支持插件级 runtime config。
3. 插件运行入口支持从对象存储拉取,并缓存到本地文件目录。
4. 输入引导配置增加校验,避免错误配置了自定义词库地址。
## 🐛 修复 ## 🐛 修复
## 🛠️ 代码优化 ## 🛠️ 代码优化
1. 将 app API 接口全部用 zod schema 编写并生成文档。 1. 插件服务从旧 `runtime` 结构调整为 pnpm workspace monorepo,拆分为 HTTP 服务入口、领域模型、用例、API adapter、基础设施、SDK 和 CLI。
2. 将 app API 接口全部用 zod schema 编写并生成文档。
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"content/faq/points_consumption.en.mdx": "2026-04-26T21:08:47+08:00", "content/faq/points_consumption.en.mdx": "2026-04-26T21:08:47+08:00",
"content/faq/points_consumption.mdx": "2026-04-26T21:08:47+08:00", "content/faq/points_consumption.mdx": "2026-04-26T21:08:47+08:00",
"content/guide/admin/sso.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/admin/sso.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/admin/sso.mdx": "2026-05-07T15:06:40+08:00", "content/guide/admin/sso.mdx": "2026-06-02T16:55:40+08:00",
"content/guide/admin/teamMode.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/admin/teamMode.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/admin/teamMode.mdx": "2026-05-07T15:06:40+08:00", "content/guide/admin/teamMode.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/evaluation.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/build/evaluation.en.mdx": "2026-05-07T15:06:40+08:00",
...@@ -283,7 +283,7 @@ ...@@ -283,7 +283,7 @@
"content/self-host/upgrading/4-15/41503.en.mdx": "2026-05-28T16:21:09+08:00", "content/self-host/upgrading/4-15/41503.en.mdx": "2026-05-28T16:21:09+08:00",
"content/self-host/upgrading/4-15/41503.mdx": "2026-05-28T16:21:09+08:00", "content/self-host/upgrading/4-15/41503.mdx": "2026-05-28T16:21:09+08:00",
"content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-01T17:19:55+08:00", "content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-01T17:19:55+08:00",
"content/self-host/upgrading/4-15/41504.mdx": "2026-06-01T17:19:55+08:00", "content/self-host/upgrading/4-15/41504.mdx": "2026-06-01T18:35:43+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",
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
"dev:pro": "turbo run dev:pro --filter=@fastgpt/app", "dev:pro": "turbo run dev:pro --filter=@fastgpt/app",
"prepare": "husky install", "prepare": "husky install",
"gen:theme-typings": "chakra-cli tokens packages/web/styles/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts", "gen:theme-typings": "chakra-cli tokens packages/web/styles/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts",
"gen:deploy": "node ./deploy/init.mjs",
"postinstall": "pnpm gen:theme-typings && pnpm run build:sdks", "postinstall": "pnpm gen:theme-typings && pnpm run build:sdks",
"initIcon": "node ./scripts/icon/init.js && prettier --config \"./.prettierrc.js\" --write \"packages/web/components/common/Icon/constants.ts\"", "initIcon": "node ./scripts/icon/init.js && prettier --config \"./.prettierrc.js\" --write \"packages/web/components/common/Icon/constants.ts\"",
"previewIcon": "node ./scripts/icon/index.js", "previewIcon": "node ./scripts/icon/index.js",
......
...@@ -32,3 +32,6 @@ export const langMap = { ...@@ -32,3 +32,6 @@ export const langMap = {
avatar: 'common/language/China' avatar: 'common/language/China'
} }
}; };
export const I18nUnionStringSchema = z.union([I18nStringSchema, z.string()]);
export type I18nUnionStringType = z.infer<typeof I18nUnionStringSchema>;
import { WorkflowIOValueTypeEnum } from '../workflow/constants'; import { WorkflowIOValueTypeEnum } from '../workflow/constants';
import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../workflow/node/constant'; import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../workflow/node/constant';
import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '../workflow/type/io'; import type { InputConfigType } from '../workflow/type/io';
import {
InputConfigInputTypeEnum,
type FlowNodeInputItemType,
type FlowNodeOutputItemType
} from '../workflow/type/io';
import SwaggerParser from '@apidevtools/swagger-parser'; import SwaggerParser from '@apidevtools/swagger-parser';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { OpenAPIV3 } from 'openapi-types'; import type { OpenAPIV3 } from 'openapi-types';
...@@ -53,7 +58,9 @@ export const JsonSchemaPropertiesItemSchema = z.object({ ...@@ -53,7 +58,9 @@ export const JsonSchemaPropertiesItemSchema = z.object({
examples: z.array(z.any()).optional(), // 示例 examples: z.array(z.any()).optional(), // 示例
// 自定义扩展(FastGPT 专用) // 自定义扩展(FastGPT 专用)
'x-tool-description': z.string().optional() // 工具描述 'x-tool-description': z.string().optional(), // 工具描述
toolDescription: z.string().optional(), // 工具描述 for System Tool
isSecret: z.boolean().optional() // System Tool
}); });
export type JsonSchemaPropertiesItemType = z.infer<typeof JsonSchemaPropertiesItemSchema>; export type JsonSchemaPropertiesItemType = z.infer<typeof JsonSchemaPropertiesItemSchema>;
...@@ -100,10 +107,18 @@ export const getNodeInputTypeFromSchemaInputType = ({ ...@@ -100,10 +107,18 @@ export const getNodeInputTypeFromSchemaInputType = ({
}; };
const getNodeInputRenderTypeFromSchemaInputType = ({ const getNodeInputRenderTypeFromSchemaInputType = ({
type, type,
items,
enum: enumList, enum: enumList,
minimum, minimum,
maximum maximum
}: JsonSchemaPropertiesItemType) => { }: JsonSchemaPropertiesItemType) => {
if (type === 'array' && items?.enum && items.enum.length > 0) {
return {
value: [],
renderTypeList: [FlowNodeInputTypeEnum.multipleSelect],
list: items.enum.map((item: any) => ({ label: item, value: item }))
};
}
if (enumList && enumList.length > 0) { if (enumList && enumList.length > 0) {
return { return {
value: enumList[0], value: enumList[0],
...@@ -130,38 +145,46 @@ const getNodeInputRenderTypeFromSchemaInputType = ({ ...@@ -130,38 +145,46 @@ const getNodeInputRenderTypeFromSchemaInputType = ({
} }
return { renderTypeList: [FlowNodeInputTypeEnum.JSONEditor, FlowNodeInputTypeEnum.reference] }; return { renderTypeList: [FlowNodeInputTypeEnum.JSONEditor, FlowNodeInputTypeEnum.reference] };
}; };
export const jsonSchema2NodeInput = ({ export const jsonSchema2NodeInput = ({
jsonSchema, jsonSchema = { type: 'Object' },
schemaType schemaType
}: { }: {
jsonSchema?: JSONSchemaInputType; jsonSchema?: JSONSchemaInputType;
schemaType: 'mcp' | 'http'; schemaType: 'mcp' | 'http' | 'systemTool';
}): FlowNodeInputItemType[] => { }): FlowNodeInputItemType[] => {
if (!jsonSchema) return []; if (!jsonSchema) return [];
return Object.entries(jsonSchema?.properties || {}).map(([key, value]) => ({ return Object.entries(jsonSchema?.properties || {}).map(([key, value]) => ({
key, key,
label: key, label: value.title || key,
valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }), valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }),
description: value.description, description: value.description,
toolDescription: schemaType === 'http' ? value['x-tool-description'] : value.description || key, toolDescription:
schemaType === 'http'
? value['x-tool-description']
: schemaType === 'systemTool'
? value['toolDescription']
: value.description || key,
required: jsonSchema?.required?.includes(key), required: jsonSchema?.required?.includes(key),
...getNodeInputRenderTypeFromSchemaInputType(value) ...getNodeInputRenderTypeFromSchemaInputType(value)
})); }));
}; };
export const jsonSchema2NodeOutput = (
jsonSchema?: JSONSchemaOutputType export const jsonSchema2NodeOutput = ({
): FlowNodeOutputItemType[] => { jsonSchema
}: { jsonSchema?: JSONSchemaOutputType } = {}): FlowNodeOutputItemType[] => {
if (!jsonSchema) return []; if (!jsonSchema) return [];
return Object.entries(jsonSchema?.properties || {}).map(([key, value]) => ({ return Object.entries(jsonSchema?.properties || {}).map(([key, value]) => ({
id: key, id: key,
key, key,
label: key, label: value.title || key,
required: jsonSchema?.required?.includes(key), required: jsonSchema?.required?.includes(key),
type: FlowNodeOutputTypeEnum.static, type: FlowNodeOutputTypeEnum.static,
valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }), valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }),
description: value.description description: value.description
})); }));
}; };
export const str2OpenApiSchema = async (yamlStr = ''): Promise<OpenApiJsonSchema> => { export const str2OpenApiSchema = async (yamlStr = ''): Promise<OpenApiJsonSchema> => {
try { try {
const data = (() => { const data = (() => {
...@@ -259,3 +282,47 @@ export const getSchemaValueType = (schema: { type: string; items?: { type: strin ...@@ -259,3 +282,47 @@ export const getSchemaValueType = (schema: { type: string; items?: { type: strin
return schema?.type as WorkflowIOValueTypeEnum; return schema?.type as WorkflowIOValueTypeEnum;
}; };
export const jsonSchema2SecretInput = ({
jsonSchema = { type: 'Object' }
}: {
jsonSchema?: JSONSchemaInputType;
}): InputConfigType[] | undefined => {
if (!jsonSchema) return undefined;
return Object.entries(jsonSchema?.properties || {}).map(([key, value]) => {
const workflowInputType = getNodeInputTypeFromSchemaInputType({
type: value.type,
arrayItems: value.items
});
// inputType => inputConfig 里面的 inputType
const inputType = (() => {
if (value?.isSecret === true) return InputConfigInputTypeEnum.secret;
switch (workflowInputType) {
case WorkflowIOValueTypeEnum.string:
return InputConfigInputTypeEnum.input;
case WorkflowIOValueTypeEnum.number:
return InputConfigInputTypeEnum.numberInput;
case WorkflowIOValueTypeEnum.boolean:
return InputConfigInputTypeEnum.switch;
case WorkflowIOValueTypeEnum.object:
return InputConfigInputTypeEnum.input;
case WorkflowIOValueTypeEnum.arrayString:
case WorkflowIOValueTypeEnum.arrayNumber:
case WorkflowIOValueTypeEnum.arrayBoolean:
case WorkflowIOValueTypeEnum.arrayObject:
case WorkflowIOValueTypeEnum.arrayAny:
return InputConfigInputTypeEnum.select;
case WorkflowIOValueTypeEnum.any:
return InputConfigInputTypeEnum.input;
}
})();
return {
inputType,
key,
label: value.title ?? key,
description: value.description,
required: jsonSchema?.required?.includes(key),
...(value.enum ? { list: value.enum.map((v) => ({ label: v, value: v })) } : {})
} satisfies InputConfigType;
});
};
...@@ -4,6 +4,10 @@ export enum AppToolSourceEnum { ...@@ -4,6 +4,10 @@ export enum AppToolSourceEnum {
commercial = 'commercial', // configured in Pro, with associatedPluginId. Specially, commercial-dalle3 is a systemTool commercial = 'commercial', // configured in Pro, with associatedPluginId. Specially, commercial-dalle3 is a systemTool
mcp = 'mcp', // mcp mcp = 'mcp', // mcp
http = 'http', // http http = 'http', // http
/** @deprecated */
community = 'community' // this is deprecated, will be replaced by systemTool /**
* will be replaced by systemTool
* @deprecated
*/
community = 'community'
} }
...@@ -74,7 +74,7 @@ export const getHTTPToolRuntimeNode = ({ ...@@ -74,7 +74,7 @@ export const getHTTPToolRuntimeNode = ({
jsonSchema: tool.requestSchema, jsonSchema: tool.requestSchema,
inputs: jsonSchema2NodeInput({ jsonSchema: tool.inputSchema, schemaType: 'http' }), inputs: jsonSchema2NodeInput({ jsonSchema: tool.inputSchema, schemaType: 'http' }),
outputs: [ outputs: [
...jsonSchema2NodeOutput(tool.outputSchema), ...jsonSchema2NodeOutput({ jsonSchema: tool.outputSchema }),
{ {
id: NodeOutputKeyEnum.rawResponse, id: NodeOutputKeyEnum.rawResponse,
key: NodeOutputKeyEnum.rawResponse, key: NodeOutputKeyEnum.rawResponse,
......
import type { LangEnum } from '../../../../common/i18n/type';
import { parseI18nString } from '../../../../common/i18n/utils';
import type { ToolListItemType } from '../../../../sdk/fastgpt-plugin';
import type { SystemPluginToolCollectionType } from '../../../plugin/tool/type';
import { PluginStatusEnum } from '../../../plugin/type';
import { SystemToolSystemSecretStatusEnum } from './constants';
import type { SystemToolListItemType } from './type';
type SystemToolConfigLike = SystemPluginToolCollectionType & {
toObject?: () => SystemPluginToolCollectionType;
};
export const SystemToolCodec = {
getDBPluginId: (pluginId: string) => `systemTool-${pluginId}`,
getPluginIdFromDB: (dbPluginId: string) => dbPluginId.replace(/^systemTool-/, ''),
getConfiguredSecretsVal(config?: SystemToolConfigLike | null) {
if (!config) return undefined;
const configData = typeof config.toObject === 'function' ? config.toObject() : config;
if (
Object.prototype.hasOwnProperty.call(configData, 'secretsVal') &&
configData.secretsVal !== undefined
) {
return configData.secretsVal ?? undefined;
}
return configData.inputListVal;
},
getSystemSecretStatus({
hasSecret,
hasSystemSecret
}: {
hasSecret?: boolean;
hasSystemSecret?: boolean;
}) {
if (!hasSecret) return SystemToolSystemSecretStatusEnum.none;
return hasSystemSecret
? SystemToolSystemSecretStatusEnum.configured
: SystemToolSystemSecretStatusEnum.unconfigured;
},
fromDBTypeToListItemType(item: SystemPluginToolCollectionType): SystemToolListItemType {
const {
name,
avatar,
intro,
toolDescription,
version,
userGuide,
author = '',
tags
} = item.customConfig!;
return {
id: item.pluginId,
version,
status: item.status ?? PluginStatusEnum.Normal,
source: 'system',
author,
name,
avatar: avatar ?? '',
intro: intro ?? '',
tags: tags ?? [],
currentCost: item.currentCost ?? 0,
hasTokenFee: item.hasTokenFee ?? false,
pluginOrder: item.pluginOrder,
userGuide,
// 数据库内配置的 system tool 一定没有 system secret
hasSystemSecret: false,
systemSecretStatus: SystemToolSystemSecretStatusEnum.none,
systemKeyCost: 0,
// 数据库里面取出来的一定不是 toolset
isToolSet: false,
toolDescription: toolDescription ?? intro ?? '',
hideTags: item.hideTags ?? [],
promoteTags: item.promoteTags ?? []
// TODO: 不知道谁做落了,之后再补吧
// courseUrl: '',
// readmeURL 没有
// readmeUrl: ''
};
},
attachToolConfig({
tool,
config,
lang
}: {
tool: ToolListItemType;
config?: SystemPluginToolCollectionType;
lang?: `${LangEnum}`;
}): SystemToolListItemType {
const configuredSecretsVal = this.getConfiguredSecretsVal(config);
const hasSystemSecret = !!configuredSecretsVal;
return {
id: this.getDBPluginId(tool.pluginId),
etag: tool.etag,
author: tool.author ?? global.feConfigs.systemTitle ?? '',
avatar: tool.icon,
currentCost: config?.currentCost ?? 0,
hasSystemSecret,
systemSecretStatus: this.getSystemSecretStatus({
hasSecret: tool.hasSecret,
hasSystemSecret
}),
hasTokenFee: config?.hasTokenFee ?? false,
intro: parseI18nString(tool.description, lang),
isToolSet: !!tool.children && tool.children.length > 0,
name: parseI18nString(tool.name, lang),
status: config?.status ?? PluginStatusEnum.Normal,
systemKeyCost: config?.systemKeyCost ?? 0,
tags: config?.customConfig?.tags ?? tool.tags ?? [],
toolDescription: config?.customConfig?.toolDescription ?? tool.toolDescription ?? '',
version: tool.version,
courseUrl: tool.tutorialUrl,
hideTags: config?.hideTags ?? [],
promoteTags: config?.promoteTags ?? [],
pluginOrder: config?.pluginOrder ?? 0,
readmeUrl: tool.readmeUrl,
source: tool.source,
userGuide: config?.customConfig?.userGuide
};
}
};
import { i18nT } from '../../../../common/i18n/utils'; import { i18nT } from '../../../../common/i18n/utils';
/**
* 系统插件密钥来源
*/
export enum SystemToolSecretInputTypeEnum { export enum SystemToolSecretInputTypeEnum {
/** 系统密钥 */
system = 'system', system = 'system',
/** 团队密钥
* @unimplemented
*/
team = 'team', team = 'team',
/**
* 自定义的
*/
manual = 'manual' manual = 'manual'
} }
export const SystemToolSecretInputTypeMap = { export const SystemToolSecretInputTypeMap = {
[SystemToolSecretInputTypeEnum.system]: { [SystemToolSecretInputTypeEnum.system]: {
text: i18nT('common:System') text: i18nT('common:System')
...@@ -16,3 +26,9 @@ export const SystemToolSecretInputTypeMap = { ...@@ -16,3 +26,9 @@ export const SystemToolSecretInputTypeMap = {
text: i18nT('common:Manual') text: i18nT('common:Manual')
} }
}; };
export enum SystemToolSystemSecretStatusEnum {
none = 'none',
configured = 'configured',
unconfigured = 'unconfigured'
}
// admin 系统管理员视角看到的系统工具的类型
import z from 'zod';
import { SystemToolBaseSchema, SystemToolDetailSchema } from './base';
import { PluginStatusSchema } from '../../../../plugin/type';
import { SystemToolSystemSecretStatusEnum } from '../constants';
export const AdminSystemToolListItemSchema = z.object({
...SystemToolBaseSchema.shape,
// 基础信息
status: PluginStatusSchema.meta({ description: '工具的状态' }),
// source: z.string().meta({ description: '工具的来源, system 或 teamId' }),
isToolSet: z.boolean().meta({ description: '是否为工具集' }),
avatar: z.string().meta({ description: '工具的图标' }),
name: z.string().meta({ description: '工具的名称' }),
intro: z.string().meta({ description: '工具的简介' }),
author: z.string().meta({ description: '工具的作者' }),
tags: z.array(z.string()).meta({ description: '工具的标签' }),
pluginOrder: z.number().optional().meta({ description: '工具的排序字段' }),
originCost: z.number().optional().meta({ description: '工具的原始费用' }),
currentCost: z.number().meta({ description: '当前使用的费用' }),
systemKeyCost: z.number().meta({ description: '系统密钥的费用' }),
hasTokenFee: z.boolean().meta({ description: '是否有系统密钥费用' }),
systemSecretStatus: z
.enum(SystemToolSystemSecretStatusEnum)
.meta({ description: '系统密钥配置状态' })
});
export type AdminSystemToolListItemType = z.infer<typeof AdminSystemToolListItemSchema>;
export const AdminSystemToolChildDetailSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
icon: z.string().optional(),
currentCost: z.number().meta({ description: '当前使用的费用' }),
systemKeyCost: z.number().meta({ description: '系统密钥的费用' })
// inputs: z.array(FlowNodeInputItemTypeSchema),
// outputs: z.array(FlowNodeOutputItemTypeSchema)
});
export type AdminSystemToolChildDetailType = z.infer<typeof AdminSystemToolChildDetailSchema>;
/** 系统工具的详细信息 */
export const AdminSystemToolDetailSchema = z.object({
...SystemToolDetailSchema.omit({
inputs: true,
outputs: true,
isLatestVersion: true,
children: true
}).shape,
children: z.array(AdminSystemToolChildDetailSchema).optional()
});
export type AdminSystemToolDetailType = z.infer<typeof AdminSystemToolDetailSchema>;
import z from 'zod';
import { PluginStatusSchema } from '../../../../plugin/type';
import { UserTagsSchema } from '../../../../../support/user/type';
import {
FlowNodeInputItemTypeSchema,
FlowNodeOutputItemTypeSchema,
InputConfigTypeSchema
} from '../../../../workflow/type/io';
import { PluginPermissionEnumSchema } from '../../../../../sdk/fastgpt-plugin';
import { SystemToolSystemSecretStatusEnum } from '../constants';
// 系统工具最基础最通用的类型
export const SystemToolBaseSchema = z.object({
id: z.string(),
version: z.string(),
etag: z.string().optional()
});
export const SystemToolRuntimeSchema = z
.object({
minPods: z.number().int().nonnegative(),
maxPods: z.number().int().positive(),
podTimeout: z.number().int().positive(),
maxConcurrentRequestsPerPod: z.number().int().positive()
})
.strict()
.refine((config) => config.minPods <= config.maxPods, {
message: 'minPods cannot be greater than maxPods',
path: ['minPods']
});
export type SystemToolRuntimeConfigType = z.infer<typeof SystemToolRuntimeSchema>;
export const SystemToolListItemSchema = z.object({
...SystemToolBaseSchema.shape,
// 基础信息
status: PluginStatusSchema.meta({ description: '工具的状态' }),
source: z.string().meta({ description: '工具的来源, system 或 teamId' }),
isToolSet: z.boolean().meta({ description: '是否为工具集' }),
avatar: z.string().meta({ description: '工具的图标' }),
name: z.string().meta({ description: '工具的名称' }),
intro: z.string().meta({ description: '工具的简介' }),
author: z.string().meta({ description: '工具的作者' }),
tags: z.array(z.string()).meta({ description: '工具的标签' }),
toolDescription: z.string().meta({ description: '给工具调用使用的工具的描述' }),
userGuide: z.string().nullish().meta({ description: '工具的使用指南(markdown 纯文本)' }),
readmeUrl: z.string().optional().meta({ description: '工具的 README 地址' }),
courseUrl: z.string().optional().meta({ description: '工具的教程地址' }),
pluginOrder: z.number().optional().meta({ description: '工具的排序字段' }),
// 计费相关
originCost: z.number().optional().meta({ description: '工具的原始费用' }), // 现在没用
currentCost: z.number().meta({ description: '当前使用的费用' }),
systemKeyCost: z.number().meta({ description: '系统密钥的费用' }),
hasTokenFee: z.boolean().meta({ description: '是否有系统密钥费用' }),
hasSystemSecret: z.boolean().meta({ description: '是否有系统密钥' }),
systemSecretStatus: z
.enum(SystemToolSystemSecretStatusEnum)
.default(SystemToolSystemSecretStatusEnum.none)
.meta({ description: '系统密钥配置状态' }),
secrets: z.array(InputConfigTypeSchema).optional(),
// 用户筛选
hideTags: z.array(UserTagsSchema).optional(),
promoteTags: z.array(UserTagsSchema).optional()
});
export type SystemToolListItemType = z.infer<typeof SystemToolListItemSchema>;
export const SystemToolChildDetailSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
toolDescription: z.string().optional(),
icon: z.string().optional(),
currentCost: z.number().meta({ description: '当前使用的费用' }),
systemKeyCost: z.number().meta({ description: '系统密钥的费用' }),
inputs: z.array(FlowNodeInputItemTypeSchema),
outputs: z.array(FlowNodeOutputItemTypeSchema)
});
export type SystemToolChildDetailType = z.infer<typeof SystemToolChildDetailSchema>;
/** 系统工具的详细信息
* TODO: input, output, secret 这些类型其实并不合理,应当是更干净的类型, 后续再迁移
*/
export const SystemToolDetailSchema = z.object({
...SystemToolListItemSchema.shape,
children: z.array(SystemToolChildDetailSchema).optional(),
inputs: z.array(FlowNodeInputItemTypeSchema).optional(),
outputs: z.array(FlowNodeOutputItemTypeSchema).optional(),
secrets: z.array(InputConfigTypeSchema).optional(),
secretsVal: z.record(z.string(), z.any()).nullish(),
isLatestVersion: z.boolean().optional(),
associatedPluginId: z.string().optional(),
permissions: z.array(PluginPermissionEnumSchema).optional()
});
export type SystemToolDetailType = z.infer<typeof SystemToolDetailSchema>;
export const SystemToolVersionSchema = z.object({
version: z.string(),
versionDescription: z.string().optional()
});
export type SystemToolVersionType = z.infer<typeof SystemToolVersionSchema>;
/**
* 系统工具的类型,按照业务分类有 list 和 detail 两种类型,原则上 list 尽量少字段,detail 的字段比较全
* 按照使用场景分:
* 1. 基础类型 所有场景下都需要的基础类型
* 2. admin 管理员:不需要 input/output 等信息,只需要价格配置、系统密钥等配置字段
* 3. team 视角:不需要 userTags 等配置字段(已经筛选过),需要 input/output,用于渲染
* 4. runtime 运行时:主要是计费和反向调用
* 5. 插件市场:基本等同于 team 的,需要下载量等
*/
// 基础的
export {
SystemToolBaseSchema,
SystemToolListItemSchema,
SystemToolDetailSchema,
SystemToolChildDetailSchema
} from './base';
export type { SystemToolListItemType, SystemToolDetailType } from './base';
// Admin 视角的
export {
AdminSystemToolListItemSchema,
AdminSystemToolDetailSchema,
AdminSystemToolChildDetailSchema
} from './admin';
export type {
AdminSystemToolDetailType,
AdminSystemToolListItemType,
AdminSystemToolChildDetailType
} from './admin';
import type { StoreEdgeItemType } from '../../workflow/type/edge'; import type { StoreEdgeItemType } from '../../workflow/type/edge';
import type { StoreNodeItemType } from '../../workflow/type/node'; import type { StoreNodeItemType } from '../../workflow/type/node';
import type { FlowNodeTemplateType } from '../../workflow/type/node';
import type { WorkflowTemplateType } from '../../workflow/type'; import type { WorkflowTemplateType } from '../../workflow/type';
import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '../../workflow/type/io'; import {
import type { I18nStringType } from '../../../common/i18n/type'; FlowNodeInputItemTypeSchema,
import type { PluginStatusType, SystemPluginToolTagType } from '../../plugin/type'; FlowNodeOutputItemTypeSchema,
import type { UserTagsEnum } from '../../../support/user/type'; InputConfigTypeSchema,
type FlowNodeInputItemType,
type FlowNodeOutputItemType
} from '../../workflow/type/io';
import {
PluginStatusSchema,
type PluginStatusType,
type SystemPluginToolTagType
} from '../../plugin/type';
import type { UserTagsType } from '../../../support/user/type';
import { UserTagsSchema } from '../../../support/user/type';
import z from 'zod';
export type AppToolRuntimeType = { export type AppToolRuntimeType = {
id: string; id: string;
...@@ -23,55 +33,13 @@ export type AppToolRuntimeType = { ...@@ -23,55 +33,13 @@ export type AppToolRuntimeType = {
hasTokenFee?: boolean; hasTokenFee?: boolean;
}; };
// System tool // // System tool
export type AppToolTemplateItemType = WorkflowTemplateType & {
status?: PluginStatusType;
// FastGPT-plugin tool
inputs?: FlowNodeInputItemType[];
outputs?: FlowNodeOutputItemType[];
versionList?: {
value: string;
description?: string;
inputs: FlowNodeInputItemType[]; // export type AppToolTemplateListItemType = Omit<
outputs: FlowNodeOutputItemType[]; // AppToolTemplateItemType,
}[]; // 'name' | 'intro' | 'workflow'
// > & {
// Admin workflow tool // name: string;
associatedPluginId?: string; // intro: string;
userGuide?: string; // tags?: SystemPluginToolTagType[];
// };
// commercial plugin config
originCost?: number; // n points/one time
currentCost?: number;
systemKeyCost?: number;
hasTokenFee?: boolean;
pluginOrder?: number;
tags?: string[] | null;
defaultInstalled?: boolean;
isOfficial?: boolean;
// Admin config
inputList?: FlowNodeInputItemType['inputList'];
inputListVal?: Record<string, any>;
hasSystemSecret?: boolean;
// User tag filtering
hideTags?: UserTagsEnum[] | null;
promoteTags?: UserTagsEnum[] | null;
/** @deprecated */
isActive?: boolean; //use tags instead
/** @deprecated */
templateType?: string;
};
export type AppToolTemplateListItemType = Omit<
AppToolTemplateItemType,
'name' | 'intro' | 'workflow'
> & {
name: string;
intro: string;
tags?: SystemPluginToolTagType[];
};
...@@ -53,7 +53,7 @@ export function splitCombineToolId(id: string): { ...@@ -53,7 +53,7 @@ export function splitCombineToolId(id: string): {
// mcp-appId, mcp-appId/toolname // mcp-appId, mcp-appId/toolname
if (source === AppToolSourceEnum.mcp) { if (source === AppToolSourceEnum.mcp) {
const [parentId, toolName] = toolId.split('/'); const [parentId] = toolId.split('/');
return { return {
source: AppToolSourceEnum.mcp, source: AppToolSourceEnum.mcp,
pluginId: toolId, pluginId: toolId,
...@@ -61,7 +61,7 @@ export function splitCombineToolId(id: string): { ...@@ -61,7 +61,7 @@ export function splitCombineToolId(id: string): {
}; };
} }
if (source === AppToolSourceEnum.http) { if (source === AppToolSourceEnum.http) {
const [parentId, toolName] = toolId.split('/'); const [parentId] = toolId.split('/');
return { return {
source: AppToolSourceEnum.http, source: AppToolSourceEnum.http,
pluginId: toolId, pluginId: toolId,
......
...@@ -9,7 +9,7 @@ import { StoreEdgeItemTypeSchema } from '../workflow/type/edge'; ...@@ -9,7 +9,7 @@ import { StoreEdgeItemTypeSchema } from '../workflow/type/edge';
import type { AppPermission } from '../../support/permission/app/controller'; import type { AppPermission } from '../../support/permission/app/controller';
import { ParentIdSchema, type ParentIdType } from '../../common/parentFolder/type'; import { ParentIdSchema, type ParentIdType } from '../../common/parentFolder/type';
import type { WorkflowTemplateBasicType } from '../workflow/type'; import type { WorkflowTemplateBasicType } from '../workflow/type';
import { UserTagsEnum, type SourceMemberType } from '../../support/user/type'; import { UserTagsSchema, type SourceMemberType } from '../../support/user/type';
import z from 'zod'; import z from 'zod';
import { ObjectIdSchema } from '../../common/type/mongo'; import { ObjectIdSchema } from '../../common/type/mongo';
import { AppFileSelectConfigTypeSchema } from './type/config.schema'; import { AppFileSelectConfigTypeSchema } from './type/config.schema';
...@@ -302,10 +302,10 @@ export const AppTemplateSchema = z.object({ ...@@ -302,10 +302,10 @@ export const AppTemplateSchema = z.object({
isPromoted: BoolSchema.optional().meta({ isPromoted: BoolSchema.optional().meta({
description: '是否推荐' description: '是否推荐'
}), }),
promoteTags: z.array(UserTagsEnum).optional().meta({ promoteTags: z.array(UserTagsSchema).optional().meta({
description: '推荐用户标签' description: '推荐用户标签'
}), }),
hideTags: z.array(UserTagsEnum).optional().meta({ hideTags: z.array(UserTagsSchema).optional().meta({
description: '隐藏用户标签' description: '隐藏用户标签'
}), }),
recommendText: z.string().optional().meta({ recommendText: z.string().optional().meta({
......
import { ParentIdSchema } from '../../../../common/parentFolder/type'; // import { ParentIdSchema } from '../../../../common/parentFolder/type';
import { SystemToolBasicConfigSchema, ToolSecretInputItemSchema } from '../../tool/type'; // import { SystemToolBasicConfigSchema, ToolSecretInputItemSchema } from '../../tool/type';
import z from 'zod'; // import z from 'zod';
import { UserTagsEnum } from '../../../../support/user/type'; // import { UserTagsSchema } from '../../../../support/user/type';
export const AdminSystemToolListItemSchema = SystemToolBasicConfigSchema.merge( // export const AdminSystemToolListItemSchema = SystemToolBasicConfigSchema.merge(
z.object({ // z.object({
id: z.string(), // id: z.string(),
parentId: ParentIdSchema, // parentId: ParentIdSchema,
name: z.string(), // name: z.string(),
intro: z.string().optional(), // intro: z.string().optional(),
author: z.string().optional(), // author: z.string().optional(),
avatar: z.string().optional(), // avatar: z.string().optional(),
tags: z.array(z.string()).nullish(), // tags: z.array(z.string()).nullish(),
hasSystemSecret: z.boolean().optional(), // hasSystemSecret: z.boolean().optional(),
// App tool // // App tool
associatedPluginId: z.string().optional(), // associatedPluginId: z.string().optional(),
isFolder: z.boolean().optional(), // isFolder: z.boolean().optional(),
hasSecretInput: z.boolean() // hasSecretInput: z.boolean()
}) // })
); // );
export type AdminSystemToolListItemType = z.infer<typeof AdminSystemToolListItemSchema>; // export type AdminSystemToolListItemType = z.infer<typeof AdminSystemToolListItemSchema>;
// Child config schema for update // // Child config schema for update
export const ToolsetChildSchema = z.object({ // export const ToolsetChildSchema = z.object({
pluginId: z.string(), // pluginId: z.string(),
name: z.string(), // name: z.string(),
systemKeyCost: z.number().optional() // systemKeyCost: z.number().optional()
}); // });
export const AdminSystemToolDetailSchema = AdminSystemToolListItemSchema.omit({ // export const AdminSystemToolDetailSchema = AdminSystemToolListItemSchema.omit({
hasSecretInput: true // hasSecretInput: true
}).extend({ // }).extend({
userGuide: z.string().nullish(), // userGuide: z.string().nullish(),
inputList: z.array(ToolSecretInputItemSchema).optional(), // inputList: z.array(ToolSecretInputItemSchema).optional(),
inputListVal: z.record(z.string(), z.any()).nullish(), // inputListVal: z.record(z.string(), z.any()).nullish(),
childTools: z.array(ToolsetChildSchema).optional(), // runtimeConfig: z.record(z.string(), z.unknown()).optional(),
promoteTags: z.array(UserTagsEnum).nullish().describe('对拥有这些 Tag 的用户推荐, 排序到前面'), // childTools: z.array(ToolsetChildSchema).optional(),
hideTags: z.array(UserTagsEnum).nullish().describe('对拥有这些 Tag 的用户隐藏') // promoteTags: z.array(UserTagsSchema).nullish().describe('对拥有这些 Tag 的用户推荐, 排序到前面'),
}); // hideTags: z.array(UserTagsSchema).nullish().describe('对拥有这些 Tag 的用户隐藏')
export type AdminSystemToolDetailType = z.infer<typeof AdminSystemToolDetailSchema>; // });
// export type AdminSystemToolDetailType = z.infer<typeof AdminSystemToolDetailSchema>;
import z from 'zod'; import z from 'zod';
import { PluginStatusEnum, PluginStatusSchema } from '../type'; import { PluginStatusEnum, PluginStatusSchema } from '../type';
import { UserTagsEnum } from '../../../support/user/type'; import { UserTagsSchema } from '../../../support/user/type';
// 无论哪种 Tool,都会有这一层配置 // 无论哪种 Tool,都会有这一层配置
export const SystemToolBasicConfigSchema = z.object({ export const SystemToolBasicConfigSchema = z.object({
defaultInstalled: z.boolean().optional(),
status: PluginStatusSchema.optional().default(PluginStatusEnum.Normal), status: PluginStatusSchema.optional().default(PluginStatusEnum.Normal),
originCost: z.number().optional(), originCost: z.number().optional(),
currentCost: z.number().optional(), currentCost: z.number().optional(),
...@@ -13,10 +12,11 @@ export const SystemToolBasicConfigSchema = z.object({ ...@@ -13,10 +12,11 @@ export const SystemToolBasicConfigSchema = z.object({
pluginOrder: z.number().optional() pluginOrder: z.number().optional()
}); });
/** SystemTool 配置数据库里面的的存储结构 */
export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.extend({ export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.extend({
pluginId: z.string(), pluginId: z.string(),
promoteTags: z.array(UserTagsEnum).nullish(), promoteTags: z.array(UserTagsSchema).nullish(),
hideTags: z.array(UserTagsEnum).nullish(), hideTags: z.array(UserTagsSchema).nullish(),
customConfig: z customConfig: z
.object({ .object({
name: z.string(), name: z.string(),
...@@ -30,9 +30,11 @@ export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.exte ...@@ -30,9 +30,11 @@ export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.exte
author: z.string().optional() author: z.string().optional()
}) })
.optional(), .optional(),
inputListVal: z.record(z.string(), z.any()).optional(), secretsVal: z.record(z.string(), z.any()).nullish(),
/** @deprecated */ /** @deprecated */
inputListVal: z.record(z.string(), z.any()).optional(),
/** @deprecated */
isActive: z.boolean().optional(), isActive: z.boolean().optional(),
/** @deprecated */ /** @deprecated */
inputConfig: z inputConfig: z
......
import z from 'zod'; import z from 'zod';
import { i18nT } from '../../common/i18n/utils'; import { i18nT } from '../../common/i18n/utils';
import { I18nUnionStringSchema } from '../../common/i18n/type';
export const I18nStringSchema = z.object({ export const I18nStringSchema = z.object({
en: z.string(), en: z.string(),
...@@ -11,19 +12,17 @@ export const I18nUnioStringSchema = z.union([I18nStringSchema, z.string()]); ...@@ -11,19 +12,17 @@ export const I18nUnioStringSchema = z.union([I18nStringSchema, z.string()]);
export const PluginToolTagSchema = z.object({ export const PluginToolTagSchema = z.object({
tagId: z.string(), tagId: z.string(),
tagName: I18nUnioStringSchema, tagName: I18nUnionStringSchema,
tagOrder: z.number(), tagOrder: z.number(),
isSystem: z.boolean() isSystem: z.boolean()
}); });
export type SystemPluginToolTagType = z.infer<typeof PluginToolTagSchema>; export type SystemPluginToolTagType = z.infer<typeof PluginToolTagSchema>;
export const PluginStatusSchema = z.union([z.literal(1), z.literal(2), z.literal(3)]); export const PluginStatusSchema = z.enum(['Normal', 'SoonOffline', 'Offline']);
export const PluginStatusEnum = PluginStatusSchema.enum;
export type PluginStatusType = z.infer<typeof PluginStatusSchema>; export type PluginStatusType = z.infer<typeof PluginStatusSchema>;
export enum PluginStatusEnum {
Normal = 1,
SoonOffline = 2,
Offline = 3
}
export const PluginStatusMap = { export const PluginStatusMap = {
[PluginStatusEnum.Normal]: { [PluginStatusEnum.Normal]: {
label: i18nT('app:toolkit_status_normal'), label: i18nT('app:toolkit_status_normal'),
......
import type { PluginTagType } from '../../sdk/fastgpt-plugin';
import { pluginTagList } from '../../sdk/fastgpt-plugin';
/**
* 过滤静态的 Tags:Plugin built-in 的 Tags 是静态的,FastGPT 系统内允许动态配置 Tags
* @param tags 传入 string 类型的 tags
* @returns 过滤后的静态的 tags
*/
export const filterPluginTags = (tags: string[]): PluginTagType[] => {
const staticTags = pluginTagList.map((tag) => tag.id);
return tags.filter((tag) => staticTags.includes(tag)) as PluginTagType[];
};
...@@ -193,7 +193,19 @@ export const InputComponentPropsTypeSchema = z.object({ ...@@ -193,7 +193,19 @@ export const InputComponentPropsTypeSchema = z.object({
}); });
export type InputComponentPropsType = z.infer<typeof InputComponentPropsTypeSchema>; export type InputComponentPropsType = z.infer<typeof InputComponentPropsTypeSchema>;
// 输入配置 export const InputConfigInputTypeSchema = z.enum([
'input',
'numberInput',
'secret',
'switch',
'select'
]);
export const InputConfigInputTypeEnum = InputConfigInputTypeSchema.enum;
export type InputConfigInputTypeType = z.infer<typeof InputConfigInputTypeSchema>;
/** 系统密钥输入配置 */
export const InputConfigTypeSchema = z.object({ export const InputConfigTypeSchema = z.object({
key: z.string().meta({ key: z.string().meta({
description: '输入配置键名' description: '输入配置键名'
...@@ -207,7 +219,7 @@ export const InputConfigTypeSchema = z.object({ ...@@ -207,7 +219,7 @@ export const InputConfigTypeSchema = z.object({
required: BoolSchema.optional().meta({ required: BoolSchema.optional().meta({
description: '该输入配置是否必填' description: '该输入配置是否必填'
}), }),
inputType: z.enum(['input', 'numberInput', 'secret', 'switch', 'select']).meta({ inputType: InputConfigInputTypeSchema.meta({
description: '输入配置渲染组件类型' description: '输入配置渲染组件类型'
}), }),
value: SecretValueTypeSchema.optional().meta({ value: SecretValueTypeSchema.optional().meta({
......
...@@ -185,7 +185,6 @@ const HandleTypeSchema = z.object({ ...@@ -185,7 +185,6 @@ const HandleTypeSchema = z.object({
// system template // system template
export const FlowNodeTemplateTypeSchema = FlowNodeCommonTypeSchema.extend({ export const FlowNodeTemplateTypeSchema = FlowNodeCommonTypeSchema.extend({
id: z.string(), id: z.string(),
templateType: z.string(),
status: PluginStatusSchema.optional(), status: PluginStatusSchema.optional(),
showSourceHandle: BoolSchema.optional(), showSourceHandle: BoolSchema.optional(),
...@@ -206,7 +205,9 @@ export const FlowNodeTemplateTypeSchema = FlowNodeCommonTypeSchema.extend({ ...@@ -206,7 +205,9 @@ export const FlowNodeTemplateTypeSchema = FlowNodeCommonTypeSchema.extend({
/** @deprecated */ /** @deprecated */
sourceHandle: HandleTypeSchema.optional(), sourceHandle: HandleTypeSchema.optional(),
/** @deprecated */ /** @deprecated */
targetHandle: HandleTypeSchema.optional() targetHandle: HandleTypeSchema.optional(),
/** @deprecated */
templateType: z.string().optional()
}); });
export type FlowNodeTemplateType = z.infer<typeof FlowNodeTemplateTypeSchema>; export type FlowNodeTemplateType = z.infer<typeof FlowNodeTemplateTypeSchema>;
...@@ -233,9 +234,12 @@ export const NodeTemplateListItemTypeSchema = z.object({ ...@@ -233,9 +234,12 @@ export const NodeTemplateListItemTypeSchema = z.object({
hasTokenFee: BoolSchema.optional(), hasTokenFee: BoolSchema.optional(),
instructions: z.string().optional(), // 使用说明 instructions: z.string().optional(), // 使用说明
courseUrl: z.string().optional(), courseUrl: z.string().optional(),
sourceMember: SourceMemberSchema.optional(), readmeUrl: z.string().optional(),
toolSource: z.enum(['uploaded', 'built-in']).optional()
sourceMember: SourceMemberSchema.optional()
// toolSource: z.enum(['uploaded', 'built-in']).optional()
}); });
export type NodeTemplateListItemType = z.infer<typeof NodeTemplateListItemTypeSchema>; export type NodeTemplateListItemType = z.infer<typeof NodeTemplateListItemTypeSchema>;
export const NodeTemplateListTypeSchema = z.array( export const NodeTemplateListTypeSchema = z.array(
z.object({ z.object({
......
...@@ -8,6 +8,7 @@ import { AppFolderPath } from './folder'; ...@@ -8,6 +8,7 @@ import { AppFolderPath } from './folder';
import { AppVersionPath } from './version'; import { AppVersionPath } from './version';
import { AppTemplatePath } from './template'; import { AppTemplatePath } from './template';
import { AppPermissionPath } from './permission'; import { AppPermissionPath } from './permission';
import { ToolPath } from './tool';
export const AppPath: OpenAPIPath = { export const AppPath: OpenAPIPath = {
...AppCommonPath, ...AppCommonPath,
...@@ -18,5 +19,6 @@ export const AppPath: OpenAPIPath = { ...@@ -18,5 +19,6 @@ export const AppPath: OpenAPIPath = {
...AppLogPath, ...AppLogPath,
...PublishChannelPath, ...PublishChannelPath,
...McpToolsPath, ...McpToolsPath,
...HttpToolsPath ...HttpToolsPath,
...ToolPath
}; };
import z from 'zod';
export const ToolDetailBodySchema = z.object({});
export const ToolDetailQuerySchema = z.object({
/** 系统工具的 ID
* - systemTool-xxxx
* - systemTool-xxxx/childId
* - comercial-xxxx,
*/
id: z.string().meta({ description: '系统工具的 ID' }),
version: z.string().optional().meta({ description: '系统工具的版本, 如果不填则返回最新版本' }),
source: z.string().optional().meta({ description: '系统工具的来源,默认为 system' })
});
export const ToolDetailResponseSchema = z.object({
// 基本信息
id: z.string(),
isToolSet: z.boolean().meta({ description: '是否为工具集' }),
tags: z.array(z.string()).nullish().meta({ description: '系统工具的标签' }),
avatar: z.string().optional().meta({ description: '系统工具的头像' }),
name: z.string().meta({ description: '系统工具的名称' }),
intro: z.string().optional().meta({ description: '系统工具的简介' }),
author: z.string().optional().meta({ description: '系统工具的作者' }),
instructions: z.string().optional().meta({ description: '使用说明 (文字)' }),
courseUrl: z.string().optional().meta({ description: '系统工具的教程链接' }),
readmeUrl: z.string().optional().meta({ description: '系统工具的 README 文档链接' }),
// 输入输出
// 计费相关
/** 这个字段暂时没有用 */
originCost: z.number().optional().meta({ description: '原始价格' }),
currentCost: z.number().optional().meta({ description: '价格' }),
hasTokenFee: z.boolean().optional().meta({ description: '是否配置了系统密钥' }),
systemKeyCost: z.number().optional().meta({ description: '系统密钥费用' })
});
export type ToolDetailBodyType = z.infer<typeof ToolDetailBodySchema>;
export type ToolDetailQueryType = z.infer<typeof ToolDetailQuerySchema>;
export type ToolDetailResponseType = z.infer<typeof ToolDetailResponseSchema>;
import { OpenAPIPath } from '../../../../type';
export const ToolDetailPath = {
'/api/core/app/tool/detail': {
summary: '获取系统工具详情'
}
} satisfies OpenAPIPath;
import { ToolDetailPath } from './detail';
export const ToolPath = {
...ToolDetailPath
// TODO
};
import z from 'zod';
export const SystemToolListItemSchema = z.object({
// 基础信息
id: z.string().meta({ description: '系统工具的 ID' }),
isToolSet: z.boolean().meta({ description: '是否为工具集' }),
avatar: z.string().meta({ description: '工具的图标' }),
name: z.string().meta({ description: '工具的名称' }),
intro: z.string().meta({ description: '工具的简介' }),
author: z.string().meta({ description: '工具的作者' }),
tags: z.array(z.string()).meta({ description: '工具的标签' }),
// 计费相关
currentCost: z.number().meta({ description: '当前使用的费用' }),
systemKeyCost: z.number().meta({ description: '系统密钥的费用' }),
hasTokenFee: z.boolean().meta({ description: '是否有系统密钥费用' })
});
export const SystemToolListBodySchema = z.object({
searchKey: z.string().optional(),
tags: z.array(z.string()).optional()
});
export const SystemToolListQuerySchema = z.object({});
export const SystemToolListResponseSchema = z.array(SystemToolListItemSchema);
export type SystemToolListBodyType = z.infer<typeof SystemToolListBodySchema>;
export type SystemToolListQueryType = z.infer<typeof SystemToolListQuerySchema>;
export type SystemToolListResponseType = z.infer<typeof SystemToolListResponseSchema>;
import { I18nStringSchema, I18nUnioStringSchema } from '../../../../core/plugin/type'; import { z } from 'zod';
import z from 'zod'; import { I18nStringSchema } from '../../../../common/i18n/type';
/* ============ Pkg Plugin ============== */ /* ============================================================================
// 1. Get Pkg Plugin Upload URL Schema * API: 上传系统插件包
export const GetPkgPluginUploadURLQuerySchema = z.object({ * Route: POST /api/core/plugin/admin/pkg/upload
filename: z.string() * Method: POST
* Description: 批量上传系统插件 .pkg 文件或包含多个 .pkg 的 .zip 文件,并返回解析后的插件信息
* Tags: ['Plugin', 'Admin', 'Write']
* ============================================================================ */
export const UploadPkgPluginBodySchema = z.object({
file: z.any().meta({
description:
'multipart/form-data file 字段,可重复传入,支持 .pkg 文件或包含多个 .pkg 的 .zip 文件'
})
}); });
export type GetPkgPluginUploadURLQueryType = z.infer<typeof GetPkgPluginUploadURLQuerySchema>;
export const GetPkgPluginUploadURLResponseSchema = z.object({ export const UploadPkgPluginResponseItemSchema = z.object({
postURL: z.string(), pluginId: z.string(),
formData: z.record(z.string(), z.string()), version: z.string(),
objectName: z.string() etag: z.string(),
type: z.string(),
author: z.string().optional(),
name: I18nStringSchema,
icon: z.string(),
tutorialUrl: z.string().url().optional(),
readmeUrl: z.string().url().optional(),
repoUrl: z.string().url().optional(),
permission: z.array(z.string()).optional(),
description: I18nStringSchema.optional(),
tags: z.array(z.string()).optional(),
versionDescription: I18nStringSchema.optional()
}); });
export type GetPkgPluginUploadURLResponseType = z.infer<typeof GetPkgPluginUploadURLResponseSchema>;
// 2. Parse Uploaded Pkg Plugin Schema export const UploadPkgPluginFailureSchema = z.object({
export const ParseUploadedPkgPluginQuerySchema = z.object({ fileName: z.string().optional(),
objectName: z.string() reason: I18nStringSchema
}); });
export type ParseUploadedPkgPluginQueryType = z.infer<typeof ParseUploadedPkgPluginQuerySchema>;
export const ParseUploadedPkgPluginResponseSchema = z.array( export const UploadPkgPluginResponseSchema = z.object({
z.object({ plugins: z.array(UploadPkgPluginResponseItemSchema),
toolId: z.string(), failed: z.array(UploadPkgPluginFailureSchema).optional()
name: I18nUnioStringSchema, });
description: I18nStringSchema, export type UploadPkgPluginResponseType = z.infer<typeof UploadPkgPluginResponseSchema>;
icon: z.string(),
parentId: z.string().optional(),
tags: z.array(z.string()).nullish()
})
);
export type ParseUploadedPkgPluginResponseType = z.infer<
typeof ParseUploadedPkgPluginResponseSchema
>;
// 3. Confirm Uploaded Pkg Plugin Schema // 3. Confirm Uploaded Pkg Plugin Schema
export const ConfirmUploadPkgPluginBodySchema = z.object({ export const ConfirmUploadPkgPluginBodySchema = z.object({
toolIds: z.array(z.string()) toolIds: z.array(
z.object({
pluginId: z.string(),
version: z.string(),
etag: z.string()
})
)
}); });
export type ConfirmUploadPkgPluginBodyType = z.infer<typeof ConfirmUploadPkgPluginBodySchema>; export type ConfirmUploadPkgPluginBodyType = z.infer<typeof ConfirmUploadPkgPluginBodySchema>;
// 4. Delete Pkg Plugin Schema
export const DeletePkgPluginQuerySchema = z.object({
toolId: z.string()
});
export type DeletePkgPluginQueryType = z.infer<typeof DeletePkgPluginQuerySchema>;
// Install plugin from url // Install plugin from url
export const InstallPluginFromUrlBodySchema = z.object({ export const InstallPluginFromUrlBodySchema = z.object({
downloadUrls: z.array(z.string()) downloadUrls: z.array(z.string())
......
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
import { import {
GetPkgPluginUploadURLQuerySchema,
GetPkgPluginUploadURLResponseSchema,
ParseUploadedPkgPluginQuerySchema,
ParseUploadedPkgPluginResponseSchema,
ConfirmUploadPkgPluginBodySchema, ConfirmUploadPkgPluginBodySchema,
DeletePkgPluginQuerySchema, InstallPluginFromUrlBodySchema,
InstallPluginFromUrlBodySchema UploadPkgPluginBodySchema,
UploadPkgPluginResponseSchema
} from './api'; } from './api';
import { TagsMap } from '../../../tag'; import { TagsMap } from '../../../tag';
import z from 'zod'; import z from 'zod';
...@@ -15,91 +12,32 @@ import { AdminPluginToolPath } from './tool'; ...@@ -15,91 +12,32 @@ import { AdminPluginToolPath } from './tool';
export const PluginAdminPath: OpenAPIPath = { export const PluginAdminPath: OpenAPIPath = {
...AdminPluginToolPath, ...AdminPluginToolPath,
// Pkg Plugin '/core/plugin/admin/pkg/upload': {
'/core/plugin/admin/pkg/presign': {
get: {
summary: '获取插件包上传预签名URL',
description: '获取插件包上传到存储服务的预签名URL,需要系统管理员权限',
tags: [TagsMap.pluginAdmin],
requestParams: {
query: GetPkgPluginUploadURLQuerySchema
},
responses: {
200: {
description: '成功获取上传URL',
content: {
'application/json': {
schema: GetPkgPluginUploadURLResponseSchema
}
}
}
}
}
},
'/core/plugin/admin/pkg/parse': {
get: {
summary: '解析已上传的插件包',
description: '解析已上传的插件包,返回插件包中包含的工具信息,需要系统管理员权限',
tags: [TagsMap.pluginAdmin],
requestParams: {
query: ParseUploadedPkgPluginQuerySchema
},
responses: {
200: {
description: '成功解析插件包',
content: {
'application/json': {
schema: ParseUploadedPkgPluginResponseSchema
}
}
}
}
}
},
'/core/plugin/admin/pkg/confirm': {
post: { post: {
summary: '确认上传插件包', summary: '批量上传系统插件包',
description: '确认上传插件包,将解析的工具添加到系统中,需要系统管理员权限', description: '上传 .pkg 文件或包含多个 .pkg 的 .zip 文件,需要系统管理员权限',
tags: [TagsMap.pluginAdmin], tags: [TagsMap.pluginAdmin],
requestBody: { requestBody: {
required: true,
content: { content: {
'application/json': { 'multipart/form-data': {
schema: ConfirmUploadPkgPluginBodySchema schema: UploadPkgPluginBodySchema
}
}
},
responses: {
200: {
description: '成功确认上传',
content: {
'application/json': {
schema: z.object({})
}
}
} }
} }
}
},
'/core/plugin/admin/pkg/delete': {
delete: {
summary: '删除插件包',
description: '删除指定的插件包工具,需要系统管理员权限',
tags: [TagsMap.pluginAdmin],
requestParams: {
query: DeletePkgPluginQuerySchema
}, },
responses: { responses: {
200: { 200: {
description: '成功删除插件包', description: '成功上传并解析插件包',
content: { content: {
'application/json': { 'application/json': {
schema: z.object({}) schema: UploadPkgPluginResponseSchema
} }
} }
} }
} }
} }
}, },
'/core/plugin/admin/installWithUrl': { '/core/plugin/admin/installWithUrl': {
post: { post: {
summary: '从URL安装插件', summary: '从URL安装插件',
......
import type { AdminSystemToolDetailSchema } from '../../../../../core/plugin/admin/tool/type';
import {
AdminSystemToolListItemSchema,
ToolsetChildSchema
} from '../../../../../core/plugin/admin/tool/type';
import z from 'zod'; import z from 'zod';
import { ParentIdSchema } from '../../../../../common/parentFolder/type'; import {
import { PluginStatusSchema } from '../../../../../core/plugin/type'; SystemToolRuntimeSchema,
import { UserTagsEnum } from '../../../../../support/user/type'; SystemToolVersionSchema
} from '../../../../../core/app/tool/systemTool/type/base';
import type { AdminSystemToolDetailType } from '../../../../../core/app/tool/systemTool/type';
import {
AdminSystemToolChildDetailSchema,
AdminSystemToolDetailSchema,
AdminSystemToolListItemSchema
} from '../../../../../core/app/tool/systemTool/type';
/* ============================================================================
* API: 获取系统工具列表
* Route: GET /api/core/plugin/admin/tool/list
* Method: GET
* Description: 获取系统工具列表,支持按工具名称关键字搜索
* Tags: ['PluginToolAdmin', 'Read']
* ============================================================================ */
// Admin tool list
export const GetAdminSystemToolsQuery = z.object({ export const GetAdminSystemToolsQuery = z.object({
parentId: ParentIdSchema searchKey: z.string().max(100).optional().meta({
example: 'search',
description: '工具名称搜索关键字'
})
}); });
export type GetAdminSystemToolsQueryType = z.infer<typeof GetAdminSystemToolsQuery>; export type GetAdminSystemToolsQueryType = z.infer<typeof GetAdminSystemToolsQuery>;
export const GetAdminSystemToolsResponseSchema = z.array(AdminSystemToolListItemSchema); export const GetAdminSystemToolsResponseSchema = z.array(AdminSystemToolListItemSchema);
...@@ -18,10 +30,45 @@ export type GetAdminSystemToolsResponseType = z.infer<typeof GetAdminSystemTools ...@@ -18,10 +30,45 @@ export type GetAdminSystemToolsResponseType = z.infer<typeof GetAdminSystemTools
// Admin tool detail // Admin tool detail
export const GetAdminSystemToolDetailQuerySchema = z.object({ export const GetAdminSystemToolDetailQuerySchema = z.object({
toolId: z.string() toolId: z.string(),
version: z.string().optional()
}); });
export type GetAdminSystemToolDetailQueryType = z.infer<typeof GetAdminSystemToolDetailQuerySchema>; export type GetAdminSystemToolDetailQueryType = z.infer<typeof GetAdminSystemToolDetailQuerySchema>;
export type GetAdminSystemToolDetailResponseType = z.infer<typeof AdminSystemToolDetailSchema>; export type GetAdminSystemToolDetailResponseType = AdminSystemToolDetailType;
// Admin tool versions
export const GetAdminSystemToolVersionsQuerySchema = z.object({
toolId: z.string()
});
export type GetAdminSystemToolVersionsQueryType = z.infer<
typeof GetAdminSystemToolVersionsQuerySchema
>;
export const GetAdminSystemToolVersionsResponseSchema = z.array(SystemToolVersionSchema);
export type GetAdminSystemToolVersionsResponseType = z.infer<
typeof GetAdminSystemToolVersionsResponseSchema
>;
// Update/reset tool runtime config
export const RuntimeConfigSchema = SystemToolRuntimeSchema;
export const GetToolRuntimeConfigQuerySchema = z.object({
pluginId: z.string()
});
export type GetToolRuntimeConfigQueryType = z.infer<typeof GetToolRuntimeConfigQuerySchema>;
export const GetToolRuntimeConfigResponseSchema = z.object({
runtimeConfig: RuntimeConfigSchema.optional()
});
export type GetToolRuntimeConfigResponseType = z.infer<typeof GetToolRuntimeConfigResponseSchema>;
export const UpdateToolRuntimeConfigBodySchema = z.object({
pluginId: z.string(),
runtimeConfig: RuntimeConfigSchema
});
export type UpdateToolRuntimeConfigBodyType = z.infer<typeof UpdateToolRuntimeConfigBodySchema>;
export const ResetToolRuntimeConfigBodySchema = z.object({
pluginId: z.string()
});
export type ResetToolRuntimeConfigBodyType = z.infer<typeof ResetToolRuntimeConfigBodySchema>;
// Update Tool Order Schema // Update Tool Order Schema
export const UpdateToolOrderBodySchema = z.object({ export const UpdateToolOrderBodySchema = z.object({
...@@ -35,32 +82,69 @@ export const UpdateToolOrderBodySchema = z.object({ ...@@ -35,32 +82,69 @@ export const UpdateToolOrderBodySchema = z.object({
export type UpdateToolOrderBodyType = z.infer<typeof UpdateToolOrderBodySchema>; export type UpdateToolOrderBodyType = z.infer<typeof UpdateToolOrderBodySchema>;
// Update system tool Schema // Update system tool Schema
const UpdateChildToolSchema = ToolsetChildSchema.omit({ const UpdateChildToolSchema = AdminSystemToolDetailSchema.shape.children.unwrap().element.pick({
name: true id: true,
systemKeyCost: true
}); });
export const UpdateToolBodySchema = z.object({ const UpdateToolSecretsValSchema = z.record(z.string(), z.any()).nullable().optional();
pluginId: z.string(),
status: PluginStatusSchema.optional(),
defaultInstalled: z.boolean().optional(),
originCost: z.number().optional(),
currentCost: z.number().nullish(),
systemKeyCost: z.number().optional(),
hasTokenFee: z.boolean().optional(),
inputListVal: z.record(z.string(), z.any()).nullish(),
childTools: z.array(UpdateChildToolSchema).optional(),
promoteTags: z.array(UserTagsEnum).nullish(),
hideTags: z.array(UserTagsEnum).nullish(),
// App tool fields export const UpdateSystemToolBodySchema = AdminSystemToolDetailSchema.pick({
name: z.string().optional(), id: true,
avatar: z.string().optional(), status: true,
intro: z.string().optional(), tags: true,
tagIds: z.array(z.string()).nullish(), currentCost: true,
associatedPluginId: z.string().optional(), systemKeyCost: true,
userGuide: z.string().nullish(), hasTokenFee: true,
author: z.string().optional() secretsVal: true,
promoteTags: true,
hideTags: true,
originCost: true
})
.partial()
.extend({
id: z.string(),
secretsVal: UpdateToolSecretsValSchema,
children: z.array(UpdateChildToolSchema).optional()
});
export type UpdateSystemToolBodyType = z.infer<typeof UpdateSystemToolBodySchema>;
// Update workflow tool Schema
export const UpdateWorkflowToolBodySchema = AdminSystemToolDetailSchema.pick({
id: true,
status: true,
name: true,
avatar: true,
intro: true,
author: true,
tags: true,
userGuide: true,
currentCost: true,
systemKeyCost: true,
hasTokenFee: true,
secretsVal: true,
promoteTags: true,
hideTags: true,
originCost: true
})
.partial()
.extend({
id: z.string(),
secretsVal: UpdateToolSecretsValSchema,
associatedPluginId: z.string().optional()
});
export type UpdateWorkflowToolBodyType = z.infer<typeof UpdateWorkflowToolBodySchema>;
// Create app type tool
export const CreateAppToolBodySchema = UpdateWorkflowToolBodySchema.omit({
id: true
}).extend({
name: z.string(),
avatar: z.string(),
intro: z.string(),
associatedPluginId: z.string(),
originCost: z.number().optional()
}); });
export type UpdateToolBodyType = z.infer<typeof UpdateToolBodySchema>; export type CreateAppToolBodyType = z.infer<typeof CreateAppToolBodySchema>;
// Delete system Tool // Delete system Tool
export const DeleteSystemToolQuerySchema = z.object({ export const DeleteSystemToolQuerySchema = z.object({
...@@ -82,9 +166,3 @@ export const GetAllSystemAppsResponseSchema = z.array( ...@@ -82,9 +166,3 @@ export const GetAllSystemAppsResponseSchema = z.array(
}) })
); );
export type GetAllSystemAppTypeToolsResponse = z.infer<typeof GetAllSystemAppsResponseSchema>; export type GetAllSystemAppTypeToolsResponse = z.infer<typeof GetAllSystemAppsResponseSchema>;
// Create app type tool
export const CreateAppToolBodySchema = UpdateToolBodySchema.omit({
childTools: true
});
export type CreateAppToolBodyType = z.infer<typeof CreateAppToolBodySchema>;
...@@ -3,16 +3,23 @@ import { ...@@ -3,16 +3,23 @@ import {
CreateAppToolBodySchema, CreateAppToolBodySchema,
DeleteSystemToolQuerySchema, DeleteSystemToolQuerySchema,
GetAdminSystemToolDetailQuerySchema, GetAdminSystemToolDetailQuerySchema,
GetAdminSystemToolVersionsQuerySchema,
GetAdminSystemToolVersionsResponseSchema,
GetAdminSystemToolsQuery, GetAdminSystemToolsQuery,
GetAdminSystemToolsResponseSchema, GetAdminSystemToolsResponseSchema,
GetAllSystemAppsBodySchema, GetAllSystemAppsBodySchema,
GetAllSystemAppsResponseSchema, GetAllSystemAppsResponseSchema,
UpdateToolBodySchema, GetToolRuntimeConfigQuerySchema,
UpdateToolOrderBodySchema GetToolRuntimeConfigResponseSchema,
ResetToolRuntimeConfigBodySchema,
UpdateSystemToolBodySchema,
UpdateToolOrderBodySchema,
UpdateToolRuntimeConfigBodySchema,
UpdateWorkflowToolBodySchema
} from './api'; } from './api';
import { TagsMap } from '../../../../tag'; import { TagsMap } from '../../../../tag';
import z from 'zod'; import { z } from 'zod';
import { AdminSystemToolDetailSchema } from '../../../../../core/plugin/admin/tool/type'; import { AdminSystemToolDetailSchema } from '../../../../../core/app/tool/systemTool/type';
import { SystemToolTagPath } from './tag'; import { SystemToolTagPath } from './tag';
export const AdminPluginToolPath: OpenAPIPath = { export const AdminPluginToolPath: OpenAPIPath = {
...@@ -56,6 +63,26 @@ export const AdminPluginToolPath: OpenAPIPath = { ...@@ -56,6 +63,26 @@ export const AdminPluginToolPath: OpenAPIPath = {
} }
} }
}, },
'/core/plugin/admin/tool/versions': {
get: {
summary: '获取系统工具版本列表',
description: '获取系统工具版本列表,需要系统管理员权限',
tags: [TagsMap.pluginToolAdmin],
requestParams: {
query: GetAdminSystemToolVersionsQuerySchema
},
responses: {
'200': {
description: '成功获取系统工具版本列表',
content: {
'application/json': {
schema: GetAdminSystemToolVersionsResponseSchema
}
}
}
}
}
},
'/core/plugin/admin/tool/update': { '/core/plugin/admin/tool/update': {
put: { put: {
summary: '更新系统工具', summary: '更新系统工具',
...@@ -65,7 +92,7 @@ export const AdminPluginToolPath: OpenAPIPath = { ...@@ -65,7 +92,7 @@ export const AdminPluginToolPath: OpenAPIPath = {
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: UpdateToolBodySchema schema: UpdateSystemToolBodySchema
} }
} }
}, },
...@@ -105,6 +132,74 @@ export const AdminPluginToolPath: OpenAPIPath = { ...@@ -105,6 +132,74 @@ export const AdminPluginToolPath: OpenAPIPath = {
} }
} }
}, },
'/core/plugin/admin/tool/runtimeConfig/update': {
put: {
summary: '更新工具运行时配置',
description: '更新插件服务中的工具运行时配置,需要系统管理员权限',
tags: [TagsMap.pluginToolAdmin],
requestBody: {
content: {
'application/json': {
schema: UpdateToolRuntimeConfigBodySchema
}
}
},
responses: {
200: {
description: '成功更新工具运行时配置',
content: {
'application/json': {
schema: z.object({})
}
}
}
}
}
},
'/core/plugin/admin/tool/runtimeConfig/detail': {
get: {
summary: '获取工具运行时配置',
description: '获取插件服务中的工具运行时配置,需要系统管理员权限',
tags: [TagsMap.pluginToolAdmin],
requestParams: {
query: GetToolRuntimeConfigQuerySchema
},
responses: {
200: {
description: '成功获取工具运行时配置',
content: {
'application/json': {
schema: GetToolRuntimeConfigResponseSchema
}
}
}
}
}
},
'/core/plugin/admin/tool/runtimeConfig/reset': {
post: {
summary: '重置工具运行时配置',
description: '将工具运行时配置重置为插件服务默认值,需要系统管理员权限',
tags: [TagsMap.pluginToolAdmin],
requestBody: {
content: {
'application/json': {
schema: ResetToolRuntimeConfigBodySchema
}
}
},
responses: {
200: {
description: '成功重置工具运行时配置',
content: {
'application/json': {
schema: z.object({})
}
}
}
}
}
},
'/core/plugin/admin/tool/delete': { '/core/plugin/admin/tool/delete': {
delete: { delete: {
summary: '删除系统工具', summary: '删除系统工具',
...@@ -126,7 +221,7 @@ export const AdminPluginToolPath: OpenAPIPath = { ...@@ -126,7 +221,7 @@ export const AdminPluginToolPath: OpenAPIPath = {
} }
}, },
// Workflow tool // Workflow tool
'/core/plugin/admin/tool/workflow/systemApps': { '/core/plugin/admin/tool/app/systemApps': {
post: { post: {
summary: '获取所有系统工具类型应用', summary: '获取所有系统工具类型应用',
description: '获取所有系统工具类型应用,用于选择系统上的应用作为系统工具。需要系统管理员权限', description: '获取所有系统工具类型应用,用于选择系统上的应用作为系统工具。需要系统管理员权限',
...@@ -150,7 +245,7 @@ export const AdminPluginToolPath: OpenAPIPath = { ...@@ -150,7 +245,7 @@ export const AdminPluginToolPath: OpenAPIPath = {
} }
} }
}, },
'/core/plugin/admin/tool/workflow/create': { '/core/plugin/admin/tool/app/create': {
post: { post: {
summary: '将系统应用设置成系统工具', summary: '将系统应用设置成系统工具',
description: '将系统应用设置成系统工具,需要系统管理员权限', description: '将系统应用设置成系统工具,需要系统管理员权限',
...@@ -174,5 +269,29 @@ export const AdminPluginToolPath: OpenAPIPath = { ...@@ -174,5 +269,29 @@ export const AdminPluginToolPath: OpenAPIPath = {
} }
} }
}, },
'/core/plugin/admin/tool/app/update': {
put: {
summary: '更新工作流工具',
description: '更新工作流工具配置,需要系统管理员权限',
tags: [TagsMap.pluginToolAdmin],
requestBody: {
content: {
'application/json': {
schema: UpdateWorkflowToolBodySchema
}
}
},
responses: {
200: {
description: '成功更新工作流工具',
content: {
'application/json': {
schema: z.object({})
}
}
}
}
}
},
...SystemToolTagPath ...SystemToolTagPath
}; };
import z from 'zod'; import z from 'zod';
import { type ToolSimpleType } from '../../../../sdk/fastgpt-plugin';
import { PaginationSchema } from '../../../api'; import { PaginationSchema } from '../../../api';
import { PluginToolTagSchema } from '../../../../core/plugin/type'; import { PluginToolTagSchema } from '../../../../core/plugin/type';
import type { ToolListItemType } from '../../../../sdk/fastgpt-plugin';
export const MarketplaceOfficialSource = 'official';
export const MarketplacePkgSourceSchema = z.string().trim().min(1);
const formatToolDetailSchema = z.object({}); const formatToolDetailSchema = z.object({});
const formatToolSimpleSchema = z.object({}); const formatToolSimpleSchema = z.object({});
// Create intersection types for extended schemas // Create intersection types for extended schemas
export const MarketplaceToolListItemSchema = formatToolSimpleSchema; export const MarketplaceToolListItemSchema = formatToolSimpleSchema;
export type MarketplaceToolListItemType = ToolSimpleType & { export type MarketplaceToolListItemType = ToolListItemType & {
toolId: string;
downloadCount: number; downloadCount: number;
downloadUrl?: string;
}; };
export const MarketplaceToolDetailItemSchema = formatToolDetailSchema.extend({ export const MarketplaceToolDetailItemSchema = formatToolDetailSchema.extend({
...@@ -34,38 +39,89 @@ export type MarketplaceToolsResponseType = z.infer<typeof MarketplaceToolsRespon ...@@ -34,38 +39,89 @@ export type MarketplaceToolsResponseType = z.infer<typeof MarketplaceToolsRespon
// Detail // Detail
export const GetMarketplaceToolDetailQuerySchema = z.object({ export const GetMarketplaceToolDetailQuerySchema = z.object({
toolId: z.string() toolId: z.string(),
version: z.string().optional()
}); });
export type GetMarketplaceToolDetailQueryType = z.infer<typeof GetMarketplaceToolDetailQuerySchema>; export type GetMarketplaceToolDetailQueryType = z.infer<typeof GetMarketplaceToolDetailQuerySchema>;
export type GetMarketplaceToolDetailResponseType = z.infer<typeof MarketplaceToolDetailSchema>; export type GetMarketplaceToolDetailResponseType = z.infer<typeof MarketplaceToolDetailSchema>;
// Upload marketplace pkg
export const UploadMarketplacePkgBodySchema = z.object({
file: z.any(),
source: MarketplacePkgSourceSchema.optional().default(MarketplaceOfficialSource)
});
export const UploadMarketplacePkgDataSchema = z.object({
source: MarketplacePkgSourceSchema.optional().default(MarketplaceOfficialSource)
});
export type UploadMarketplacePkgDataType = z.infer<typeof UploadMarketplacePkgDataSchema>;
export const UploadMarketplacePkgResponseSchema = z.object({
pluginId: z.string(),
version: z.string(),
etag: z.string(),
source: MarketplacePkgSourceSchema,
downloadUrl: z.string(),
tool: z.record(z.string(), z.unknown())
});
export type UploadMarketplacePkgResponseType = z.infer<typeof UploadMarketplacePkgResponseSchema>;
/* ============================================================================
* API: 删除 marketplace 插件
* Route: POST /marketplace/api/admin/pkg/delete
* Method: POST
* Description: 手动删除指定来源下某个插件版本的 marketplace 记录及存储文件
* Tags: ['Plugin', 'Marketplace', 'Admin', 'Delete']
* ============================================================================ */
export const DeleteMarketplacePkgBodySchema = z.object({
pluginId: z.string().trim().min(1).meta({
example: 'fastgpt-tool',
description: '插件 ID'
}),
version: z.string().trim().min(1).meta({
example: '1.0.0',
description: '插件版本'
}),
source: MarketplacePkgSourceSchema.optional().default(MarketplaceOfficialSource).meta({
example: MarketplaceOfficialSource,
description: '插件来源, 默认 official'
})
});
export type DeleteMarketplacePkgBodyType = z.infer<typeof DeleteMarketplacePkgBodySchema>;
export const DeleteMarketplacePkgResponseSchema = z.object({
pluginId: z.string().meta({ example: 'fastgpt-tool', description: '插件 ID' }),
version: z.string().meta({ example: '1.0.0', description: '插件版本' }),
source: MarketplacePkgSourceSchema.meta({
example: MarketplaceOfficialSource,
description: '插件来源'
})
});
export type DeleteMarketplacePkgResponseType = z.infer<
typeof DeleteMarketplacePkgResponseSchema
>;
// Tags // Tags
export const GetMarketplaceToolTagsResponseSchema = z.array(PluginToolTagSchema); export const GetMarketplaceToolTagsResponseSchema = z.array(PluginToolTagSchema);
export type GetMarketplaceToolTagsResponseType = z.infer< export type GetMarketplaceToolTagsResponseType = z.infer<
typeof GetMarketplaceToolTagsResponseSchema typeof GetMarketplaceToolTagsResponseSchema
>; >;
// Get installed plugins // Versions
export const GetSystemInstalledPluginsQuerySchema = z.object({ export const GetMarketplaceToolVersionsQuerySchema = z.object({
type: z.enum(['tool']).optional() toolId: z.string().optional()
}); });
export type GetSystemInstalledPluginsQueryType = z.infer< export type GetMarketplaceToolVersionsQueryType = z.infer<
typeof GetSystemInstalledPluginsQuerySchema typeof GetMarketplaceToolVersionsQuerySchema
>; >;
export const GetSystemInstalledPluginsResponseSchema = z.object({ export const MarketplaceToolVersionSchema = z.object({
list: z.array( toolId: z.string(),
z.object({
id: z.string(),
version: z.string(), version: z.string(),
name: z.any().optional(), etag: z.string().optional()
description: z.any().optional(),
icon: z.string().optional(),
author: z.string().optional(),
tags: z.array(z.string()).optional()
})
)
}); });
export type GetSystemInstalledPluginsResponseType = z.infer< export type MarketplaceToolVersionType = z.infer<typeof MarketplaceToolVersionSchema>;
typeof GetSystemInstalledPluginsResponseSchema export const GetMarketplaceToolVersionsResponseSchema = z.array(MarketplaceToolVersionSchema);
export type GetMarketplaceToolVersionsResponseType = z.infer<
typeof GetMarketplaceToolVersionsResponseSchema
>; >;
...@@ -4,32 +4,17 @@ import { ...@@ -4,32 +4,17 @@ import {
GetMarketplaceToolsBodySchema, GetMarketplaceToolsBodySchema,
MarketplaceToolDetailSchema, MarketplaceToolDetailSchema,
MarketplaceToolsResponseSchema, MarketplaceToolsResponseSchema,
UploadMarketplacePkgBodySchema,
UploadMarketplacePkgResponseSchema,
DeleteMarketplacePkgBodySchema,
DeleteMarketplacePkgResponseSchema,
GetMarketplaceToolTagsResponseSchema, GetMarketplaceToolTagsResponseSchema,
GetSystemInstalledPluginsQuerySchema, GetMarketplaceToolVersionsQuerySchema,
GetSystemInstalledPluginsResponseSchema GetMarketplaceToolVersionsResponseSchema
} from './api'; } from './api';
import { TagsMap } from '../../../tag'; import { TagsMap } from '../../../tag';
export const MarketplacePath: OpenAPIPath = { export const MarketplacePath: OpenAPIPath = {
'/core/plugin/admin/marketplace/installed': {
get: {
summary: '获取系统已安装插件的 ID 列表(管理员视角)',
tags: [TagsMap.pluginMarketplace],
requestParams: {
query: GetSystemInstalledPluginsQuerySchema
},
responses: {
200: {
description: '获取系统已安装插件的 ID 列表成功',
content: {
'application/json': {
schema: GetSystemInstalledPluginsResponseSchema
}
}
}
}
}
},
'/marketplace/api/tool/list': { '/marketplace/api/tool/list': {
get: { get: {
summary: '获取工具列表', summary: '获取工具列表',
...@@ -83,5 +68,74 @@ export const MarketplacePath: OpenAPIPath = { ...@@ -83,5 +68,74 @@ export const MarketplacePath: OpenAPIPath = {
} }
} }
} }
},
'/marketplace/api/tool/versions': {
get: {
summary: '获取工具版本列表',
tags: [TagsMap.pluginMarketplace],
requestParams: {
query: GetMarketplaceToolVersionsQuerySchema
},
responses: {
200: {
description: '获取工具版本列表成功',
content: {
'application/json': {
schema: GetMarketplaceToolVersionsResponseSchema
}
}
}
}
}
},
'/marketplace/api/admin/pkg/upload': {
post: {
summary: '上传 marketplace 插件 pkg',
tags: [TagsMap.pluginMarketplace],
requestBody: {
description: 'multipart/form-data, file 字段为 .pkg 文件',
required: true,
content: {
'multipart/form-data': {
schema: UploadMarketplacePkgBodySchema
}
}
},
responses: {
200: {
description: '上传 marketplace 插件 pkg 成功',
content: {
'application/json': {
schema: UploadMarketplacePkgResponseSchema
}
}
}
}
}
},
'/marketplace/api/admin/pkg/delete': {
post: {
summary: '删除 marketplace 插件 pkg',
tags: [TagsMap.pluginMarketplace],
requestBody: {
description: '指定 pluginId、version 与来源删除某个插件版本',
required: true,
content: {
'application/json': {
schema: DeleteMarketplacePkgBodySchema
}
}
},
responses: {
200: {
description: '删除 marketplace 插件 pkg 成功',
content: {
'application/json': {
schema: DeleteMarketplacePkgResponseSchema
}
}
}
}
}
} }
}; };
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
import { GetTeamPluginListResponseSchema, ToggleInstallPluginBodySchema } from './api';
import { TagsMap } from '../../../tag'; import { TagsMap } from '../../../tag';
import { GetTeamToolDetailQuerySchema, TeamToolDetailSchema } from './toolApi'; import {
GetTeamPluginListResponseSchema,
GetTeamToolDetailQuerySchema,
GetTeamToolVersionsQuerySchema,
GetTeamToolVersionsResponseSchema,
TeamToolDetailSchema
} from './tool/dto';
export const PluginTeamPath: OpenAPIPath = { export const PluginTeamPath: OpenAPIPath = {
'/core/plugin/team/list': { '/core/plugin/team/tool/list': {
get: { get: {
summary: '获取团队插件列表', summary: '获取团队插件列表',
description: '获取团队插件列表', description: '获取团队插件列表',
...@@ -21,42 +26,41 @@ export const PluginTeamPath: OpenAPIPath = { ...@@ -21,42 +26,41 @@ export const PluginTeamPath: OpenAPIPath = {
} }
} }
}, },
'/core/plugin/team/toggleInstall': { // Tool
post: { '/core/plugin/team/tool/detail': {
summary: '切换插件安装状态', get: {
description: '切换团队插件的安装状态,支持安装或卸载插件', summary: '获取工具卡片详情',
description: '获取工具片详情',
tags: [TagsMap.pluginTeam], tags: [TagsMap.pluginTeam],
requestBody: { requestParams: {
query: GetTeamToolDetailQuerySchema
},
responses: {
200: {
description: '获取工具卡片详情成功',
content: { content: {
'application/json': { 'application/json': {
schema: ToggleInstallPluginBodySchema schema: TeamToolDetailSchema
} }
} }
},
responses: {
200: {
description: '请求成功',
content: {}
} }
} }
} }
}, },
'/core/plugin/team/tool/versions': {
// Tool
'/core/plugin/team/toolDetail': {
get: { get: {
summary: '获取工具卡片详情', summary: '获取团队工具版本列表',
description: '获取工具片详情', description: '获取团队工具版本列表',
tags: [TagsMap.pluginTeam], tags: [TagsMap.pluginTeam],
requestParams: { requestParams: {
query: GetTeamToolDetailQuerySchema query: GetTeamToolVersionsQuerySchema
}, },
responses: { responses: {
200: { 200: {
description: '获取工具卡片详情成功', description: '获取团队工具版本列表成功',
content: { content: {
'application/json': { 'application/json': {
schema: TeamToolDetailSchema schema: GetTeamToolVersionsResponseSchema
} }
} }
} }
......
import z from 'zod';
import {
SystemToolChildDetailSchema,
SystemToolDetailSchema,
SystemToolListItemSchema
} from '../../../../../core/app/tool/systemTool/type';
import { SystemToolVersionSchema } from '../../../../../core/app/tool/systemTool/type/base';
export const GetTeamSystemPluginListQuerySchema = z.object({});
export type GetTeamSystemPluginListQueryType = z.infer<typeof GetTeamSystemPluginListQuerySchema>;
export const TeamSystemPluginListItemSchema = SystemToolListItemSchema.extend({
isPromoted: z.boolean().optional()
});
export const GetTeamPluginListResponseSchema = z.array(TeamSystemPluginListItemSchema);
export type GetTeamPluginListResponseType = z.infer<typeof GetTeamPluginListResponseSchema>;
export const GetTeamToolDetailSourceEnum = z.enum(['system', 'team']);
export const GetTeamToolDetailQuerySchema = z.object({
toolId: z.string(),
version: z.string().optional(),
source: GetTeamToolDetailSourceEnum.optional()
});
export type GetTeamToolDetailQueryType = z.infer<typeof GetTeamToolDetailQuerySchema>;
export const TeamToolDetailSchema = z.object({
...SystemToolDetailSchema.omit({
associatedPluginId: true,
hideTags: true,
secretsVal: true,
promoteTags: true,
children: true // override
}).shape,
children: z.array(SystemToolChildDetailSchema).optional()
});
export type GetTeamToolDetailResponseType = z.infer<typeof TeamToolDetailSchema>;
export const GetTeamToolVersionsQuerySchema = z.object({
toolId: z.string(),
source: GetTeamToolDetailSourceEnum.optional()
});
export type GetTeamToolVersionsQueryType = z.infer<typeof GetTeamToolVersionsQuerySchema>;
export const GetTeamToolVersionsResponseSchema = z.array(SystemToolVersionSchema);
export type GetTeamToolVersionsResponseType = z.infer<typeof GetTeamToolVersionsResponseSchema>;
import z from 'zod'; // import z from 'zod';
// export const PluginGetAccessTokenBodySchema = z.object({
// toolId: z.string(),
// teamId: z.string(),
// tmbId: z.string()
// });
export const PluginGetAccessTokenBodySchema = z.object({ // export const PluginGetAccessTokenResponseSchema = z.object({
toolId: z.string(), // accessToken: z.string()
teamId: z.string(), // });
tmbId: z.string()
});
export const PluginGetAccessTokenResponseSchema = z.object({ // export type PluginGetAccessTokenBodyType = z.infer<typeof PluginGetAccessTokenBodySchema>;
accessToken: z.string() // export type PluginGetAccessTokenResponseType = z.infer<typeof PluginGetAccessTokenResponseSchema>;
});
export type PluginGetAccessTokenBodyType = z.infer<typeof PluginGetAccessTokenBodySchema>;
export type PluginGetAccessTokenResponseType = z.infer<typeof PluginGetAccessTokenResponseSchema>;
import z from 'zod'; import z from 'zod';
/* ============================================================================
* API: 获取反向调用用户信息
* Route: POST /api/invoke/userInfo
* Method: POST
* Description: 通过 invoke token 获取当前运行上下文的用户信息
* Tags: ['Plugin', 'Invoke', 'Read']
* ============================================================================ */
export const InvokeUserInfoBodySchema = z.object({}); export const InvokeUserInfoBodySchema = z.object({});
export const InvokeUserInfoQuerySchema = z.object({}); export const InvokeUserInfoQuerySchema = z.object({});
...@@ -19,3 +27,44 @@ export const InvokeUserInfoResponseSchema = z.object({ ...@@ -19,3 +27,44 @@ export const InvokeUserInfoResponseSchema = z.object({
export type InvokeUserInfoBodyType = z.infer<typeof InvokeUserInfoBodySchema>; export type InvokeUserInfoBodyType = z.infer<typeof InvokeUserInfoBodySchema>;
export type InvokeUserInfoQueryType = z.infer<typeof InvokeUserInfoQuerySchema>; export type InvokeUserInfoQueryType = z.infer<typeof InvokeUserInfoQuerySchema>;
export type InvokeUserInfoResponseType = z.infer<typeof InvokeUserInfoResponseSchema>; export type InvokeUserInfoResponseType = z.infer<typeof InvokeUserInfoResponseSchema>;
/* ============================================================================
* API: 获取反向调用企微企业访问凭证
* Route: POST /api/invoke/wecom/corpToken
* Method: POST
* Description: 通过 invoke token 获取当前运行团队的企微企业短期访问凭证
* Tags: ['Plugin', 'Invoke', 'Wecom', 'Read']
* ============================================================================ */
export const InvokeWecomCorpTokenBodySchema = z.object({});
export const InvokeWecomCorpTokenQuerySchema = z.object({});
export const InvokeWecomCorpTokenResponseSchema = z.object({
accessToken: z.string().describe('企微企业访问凭证'),
expiresIn: z.number().describe('凭证有效期,单位秒')
});
export type InvokeWecomCorpTokenBodyType = z.infer<typeof InvokeWecomCorpTokenBodySchema>;
export type InvokeWecomCorpTokenQueryType = z.infer<typeof InvokeWecomCorpTokenQuerySchema>;
export type InvokeWecomCorpTokenResponseType = z.infer<
typeof InvokeWecomCorpTokenResponseSchema
>;
/* ============================================================================
* API: 反向调用文件上传
* Route: POST /api/invoke/fileUpload
* Method: POST
* Description: 通过 invoke token 上传 multipart/form-data 文件到当前对话文件目录
* Tags: ['Plugin', 'Invoke', 'Write']
* ============================================================================ */
export const InvokeFileUploadBodySchema = z.object({});
export const InvokeFileUploadQuerySchema = z.object({});
export const InvokeFileUploadResponseSchema = z.object({
url: z.string().describe('上传后的文件访问 URL')
});
export type InvokeFileUploadBodyType = z.infer<typeof InvokeFileUploadBodySchema>;
export type InvokeFileUploadQueryType = z.infer<typeof InvokeFileUploadQuerySchema>;
export type InvokeFileUploadResponseType = z.infer<typeof InvokeFileUploadResponseSchema>;
...@@ -11,9 +11,9 @@ ...@@ -11,9 +11,9 @@
"pnpm": "10.x" "pnpm": "10.x"
}, },
"dependencies": { "dependencies": {
"@fastgpt-sdk/plugin": "0.6.1",
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8", "@bany/curl-to-json": "^1.2.8",
"@fastgpt-plugin/sdk-client": "0.0.1-alpha.8",
"axios": "catalog:", "axios": "catalog:",
"ipaddr.js": "catalog:", "ipaddr.js": "catalog:",
"cron-parser": "^4.9.0", "cron-parser": "^4.9.0",
...@@ -22,18 +22,18 @@ ...@@ -22,18 +22,18 @@
"js-yaml": "catalog:", "js-yaml": "catalog:",
"jschardet": "3.1.1", "jschardet": "3.1.1",
"json5": "catalog:", "json5": "catalog:",
"lodash": "catalog:",
"nanoid": "catalog:", "nanoid": "catalog:",
"next": "catalog:", "next": "catalog:",
"openai": "6.34.0", "openai": "6.34.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"timezones-list": "^3.0.2", "timezones-list": "^3.0.2",
"lodash": "catalog:",
"zod": "catalog:", "zod": "catalog:",
"zod-openapi": "^5.4.6" "zod-openapi": "^5.4.6"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "catalog:",
"@types/js-yaml": "catalog:", "@types/js-yaml": "catalog:",
"@types/lodash": "catalog:",
"@types/node": "catalog:" "@types/node": "catalog:"
} }
} }
import { PluginPermissionEnumSchema } from '@fastgpt-plugin/sdk-client';
import z from 'zod';
export * from '@fastgpt-plugin/sdk-client';
export { export {
FastGPTPluginClient, PluginPermissionEnum,
RunToolWithStream, PluginPermissionEnumSchema,
ToolDetailSchema, FastGPTPluginClient
ToolSimpleSchema, } from '@fastgpt-plugin/sdk-client';
ToolTagsNameMap
} from '@fastgpt-sdk/plugin'; export const PluginPermissionListSchema = z.array(PluginPermissionEnumSchema);
export type {
AIProxyChannelsType,
I18nStringStrictType,
ToolDetailType,
ToolSimpleType
} from '@fastgpt-sdk/plugin';
...@@ -5,8 +5,9 @@ import type { UserStatusEnum } from './constant'; ...@@ -5,8 +5,9 @@ import type { UserStatusEnum } from './constant';
import { TeamMemberStatusEnum } from './team/constant'; import { TeamMemberStatusEnum } from './team/constant';
import { TeamTmbItemSchema } from './team/type'; import { TeamTmbItemSchema } from './team/type';
export const UserTagsEnum = z.enum(['wecom']); export const UserTagsSchema = z.enum(['wecom']);
export type UserTagsEnum = z.infer<typeof UserTagsEnum>; export const UserTagsEnum = UserTagsSchema.enum;
export type UserTagsType = z.infer<typeof UserTagsSchema>;
export type UserMetaType = { export type UserMetaType = {
isActivatedWecomLicense?: boolean; isActivatedWecomLicense?: boolean;
...@@ -29,7 +30,7 @@ export type UserModelSchema = { ...@@ -29,7 +30,7 @@ export type UserModelSchema = {
keyword: string; keyword: string;
}; };
contact?: string; contact?: string;
tags: UserTagsEnum[]; tags: UserTagsType[];
meta?: UserMetaType; meta?: UserMetaType;
}; };
...@@ -43,7 +44,7 @@ export const UserSchema = z.object({ ...@@ -43,7 +44,7 @@ export const UserSchema = z.object({
team: TeamTmbItemSchema, team: TeamTmbItemSchema,
permission: z.instanceof(TeamPermission), permission: z.instanceof(TeamPermission),
contact: z.string().optional(), contact: z.string().optional(),
tags: z.array(UserTagsEnum).optional() tags: z.array(UserTagsSchema).optional()
}); });
export type UserType = z.infer<typeof UserSchema>; export type UserType = z.infer<typeof UserSchema>;
......
...@@ -173,6 +173,46 @@ describe('jsonSchema2NodeInput', () => { ...@@ -173,6 +173,46 @@ describe('jsonSchema2NodeInput', () => {
expect(result).toEqual(expectResponse); expect(result).toEqual(expectResponse);
}); });
it('should return multiple select node input for array enum items', () => {
const jsonSchema: JSONSchemaInputType = {
type: 'object',
properties: {
sources: {
type: 'array',
items: {
type: 'string',
enum: ['36kr', 'zhihu', 'weibo', 'juejin', 'toutiao']
},
title: '热榜来源',
description: '选择热榜来源网站(可多选)'
}
},
required: ['sources']
};
const result = jsonSchema2NodeInput({ jsonSchema, schemaType: 'mcp' });
expect(result).toEqual([
{
key: 'sources',
label: '热榜来源',
valueType: WorkflowIOValueTypeEnum.arrayString,
description: '选择热榜来源网站(可多选)',
toolDescription: '选择热榜来源网站(可多选)',
required: true,
value: [],
renderTypeList: ['multipleSelect'],
list: [
{ label: '36kr', value: '36kr' },
{ label: 'zhihu', value: 'zhihu' },
{ label: 'weibo', value: 'weibo' },
{ label: 'juejin', value: 'juejin' },
{ label: 'toutiao', value: 'toutiao' }
]
}
]);
});
}); });
describe('getNodeInputTypeFromSchemaInputType', () => { describe('getNodeInputTypeFromSchemaInputType', () => {
...@@ -378,7 +418,7 @@ describe('jsonSchema2NodeOutput', () => { ...@@ -378,7 +418,7 @@ describe('jsonSchema2NodeOutput', () => {
const jsonSchema: JSONSchemaOutputType = { const jsonSchema: JSONSchemaOutputType = {
type: 'object' type: 'object'
}; };
const result = jsonSchema2NodeOutput(jsonSchema); const result = jsonSchema2NodeOutput({ jsonSchema });
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
...@@ -391,7 +431,7 @@ describe('jsonSchema2NodeOutput', () => { ...@@ -391,7 +431,7 @@ describe('jsonSchema2NodeOutput', () => {
}, },
required: ['result'] required: ['result']
}; };
const result = jsonSchema2NodeOutput(jsonSchema); const result = jsonSchema2NodeOutput({ jsonSchema });
expect(result).toHaveLength(2); expect(result).toHaveLength(2);
expect(result[0]).toMatchObject({ expect(result[0]).toMatchObject({
...@@ -423,7 +463,7 @@ describe('jsonSchema2NodeOutput', () => { ...@@ -423,7 +463,7 @@ describe('jsonSchema2NodeOutput', () => {
} }
} }
}; };
const result = jsonSchema2NodeOutput(jsonSchema); const result = jsonSchema2NodeOutput({ jsonSchema });
expect(result[0].description).toBe('Data object'); expect(result[0].description).toBe('Data object');
}); });
...@@ -435,7 +475,7 @@ describe('jsonSchema2NodeOutput', () => { ...@@ -435,7 +475,7 @@ describe('jsonSchema2NodeOutput', () => {
items: { type: 'array', items: { type: 'string' } } items: { type: 'array', items: { type: 'string' } }
} }
}; };
const result = jsonSchema2NodeOutput(jsonSchema); const result = jsonSchema2NodeOutput({ jsonSchema });
expect(result[0].valueType).toBe(WorkflowIOValueTypeEnum.arrayString); expect(result[0].valueType).toBe(WorkflowIOValueTypeEnum.arrayString);
}); });
......
import { describe, expect, it } from 'vitest';
import { SystemToolCodec } from '@fastgpt/global/core/app/tool/systemTool/codec';
import { UpdateSystemToolBodySchema } from '@fastgpt/global/openapi/core/plugin/admin/tool/api';
import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type';
describe('system tool config', () => {
it('allows null secretsVal to explicitly disable system secret', () => {
const result = UpdateSystemToolBodySchema.parse({
id: 'systemTool-github',
secretsVal: null
});
expect(result.secretsVal).toBeNull();
});
it('keeps missing secretsVal as no-op for partial update', () => {
const result = UpdateSystemToolBodySchema.parse({
id: 'systemTool-github'
});
expect(Object.prototype.hasOwnProperty.call(result, 'secretsVal')).toBe(false);
});
it('falls back to deprecated inputListVal only when secretsVal is absent', () => {
const legacyConfig = {
pluginId: 'systemTool-github',
inputListVal: {
token: 'legacy-token'
}
} satisfies SystemPluginToolCollectionType;
const disabledConfig = {
...legacyConfig,
secretsVal: null
} satisfies SystemPluginToolCollectionType;
expect(SystemToolCodec.getConfiguredSecretsVal(legacyConfig)).toEqual({
token: 'legacy-token'
});
expect(SystemToolCodec.getConfiguredSecretsVal(disabledConfig)).toBeUndefined();
});
it('falls back to deprecated inputListVal when secretsVal is undefined', () => {
const config = {
pluginId: 'systemTool-github',
secretsVal: undefined,
inputListVal: {
token: 'legacy-token'
}
} satisfies SystemPluginToolCollectionType;
expect(SystemToolCodec.getConfiguredSecretsVal(config)).toEqual({
token: 'legacy-token'
});
});
});
import { SystemCacheKeyEnum } from './type'; import { SystemCacheKeyEnum } from './type';
import { refreshSystemTools } from '../../core/app/tool/controller';
export const initCache = () => { export const initCache = () => {
global.systemCache = { global.systemCache = {
[SystemCacheKeyEnum.systemTool]: {
versionKey: '',
data: [],
refreshFunc: refreshSystemTools,
devRefresh: true
},
[SystemCacheKeyEnum.modelPermission]: { [SystemCacheKeyEnum.modelPermission]: {
versionKey: '', versionKey: '',
data: null, data: null,
......
import type { AppToolTemplateItemType } from '@fastgpt/global/core/app/tool/type';
export enum SystemCacheKeyEnum { export enum SystemCacheKeyEnum {
systemTool = 'systemTool',
modelPermission = 'modelPermission' modelPermission = 'modelPermission'
} }
export type SystemCacheDataType = { export type SystemCacheDataType = {
[SystemCacheKeyEnum.systemTool]: AppToolTemplateItemType[];
[SystemCacheKeyEnum.modelPermission]: null; [SystemCacheKeyEnum.modelPermission]: null;
}; };
......
...@@ -6,7 +6,11 @@ import type { ...@@ -6,7 +6,11 @@ import type {
EmbeddingModelItemType, EmbeddingModelItemType,
LLMModelItemType LLMModelItemType
} from '@fastgpt/global/core/ai/model.schema'; } from '@fastgpt/global/core/ai/model.schema';
import type { AIProxyChannelsType, I18nStringStrictType } from '@fastgpt/global/sdk/fastgpt-plugin'; import type {
I18nStringStrictType,
AIProxyChannelsType,
AiproxyMapProviderItemType
} from '@fastgpt/global/sdk/fastgpt-plugin';
import type { langType, ModelProviderItemType } from '@fastgpt/global/core/ai/provider'; import type { langType, ModelProviderItemType } from '@fastgpt/global/core/ai/provider';
export type SystemModelSchemaType = { export type SystemModelSchemaType = {
...@@ -38,7 +42,7 @@ declare global { ...@@ -38,7 +42,7 @@ declare global {
var ModelProviderRawCache: { provider: string; value: I18nStringStrictType; avatar: string }[]; var ModelProviderRawCache: { provider: string; value: I18nStringStrictType; avatar: string }[];
var ModelProviderListCache: Record<langType, ModelProviderItemType[]>; var ModelProviderListCache: Record<langType, ModelProviderItemType[]>;
var ModelProviderMapCache: Record<langType, Record<string, ModelProviderItemType>>; var ModelProviderMapCache: Record<langType, Record<string, ModelProviderItemType>>;
var aiproxyChannelsCache: AIProxyChannelsType; var aiproxyChannelsCache: AiproxyMapProviderItemType[];
var systemModelList: SystemModelItemType[]; var systemModelList: SystemModelItemType[];
// var systemModelMap: Map<string, SystemModelItemType>; // var systemModelMap: Map<string, SystemModelItemType>;
......
import { type AppSchemaType } from '@fastgpt/global/core/app/type'; import { type AppSchemaType } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { import {
FlowNodeInputTypeEnum, FlowNodeInputTypeEnum,
...@@ -31,6 +32,7 @@ import { MongoAppRecord } from './record/schema'; ...@@ -31,6 +32,7 @@ import { MongoAppRecord } from './record/schema';
import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { getLogger, LogCategories } from '../../common/logger'; import { getLogger, LogCategories } from '../../common/logger';
import { deleteSandboxesByAppId, deleteSandboxesByChatIds } from '../ai/sandbox/service/resource'; import { deleteSandboxesByAppId, deleteSandboxesByChatIds } from '../ai/sandbox/service/resource';
import { MongoSystemTool } from '../plugin/tool/systemToolSchema';
const logger = getLogger(LogCategories.MODULE.APP.FOLDER); const logger = getLogger(LogCategories.MODULE.APP.FOLDER);
...@@ -142,6 +144,15 @@ export const getAppBasicInfoByIds = async ({ teamId, ids }: { teamId: string; id ...@@ -142,6 +144,15 @@ export const getAppBasicInfoByIds = async ({ teamId, ids }: { teamId: string; id
})); }));
}; };
const cleanupWorkflowToolSystemToolAssociation = async (appIds: string[]) => {
if (appIds.length === 0) return;
await MongoSystemTool.updateMany(
{ 'customConfig.associatedPluginId': { $in: appIds } },
{ $unset: { 'customConfig.associatedPluginId': '' } }
);
};
export const deleteAppDataProcessor = async ({ export const deleteAppDataProcessor = async ({
app, app,
teamId teamId
...@@ -151,6 +162,10 @@ export const deleteAppDataProcessor = async ({ ...@@ -151,6 +162,10 @@ export const deleteAppDataProcessor = async ({
}) => { }) => {
const appId = String(app._id); const appId = String(app._id);
if (app.type === AppTypeEnum.workflowTool) {
await cleanupWorkflowToolSystemToolAssociation([appId]);
}
// 1. 删除应用头像 // 1. 删除应用头像
await removeImageByPath(app.avatar); await removeImageByPath(app.avatar);
...@@ -212,6 +227,17 @@ export const deleteAppsImmediate = async ({ ...@@ -212,6 +227,17 @@ export const deleteAppsImmediate = async ({
teamId: string; teamId: string;
appIds: string[]; appIds: string[];
}) => { }) => {
const workflowToolApps = await MongoApp.find(
{
teamId,
_id: { $in: appIds },
type: AppTypeEnum.workflowTool
},
'_id'
).lean();
await cleanupWorkflowToolSystemToolAssociation(workflowToolApps.map((app) => String(app._id)));
// Remove eval job // Remove eval job
const evalJobs = await MongoEvaluation.find( const evalJobs = await MongoEvaluation.find(
{ {
......
import { type AppTemplateSchemaType } from '@fastgpt/global/core/app/type'; import { type AppTemplateSchemaType } from '@fastgpt/global/core/app/type';
import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; import { connectionMongo, getMongoModel } from '../../../common/mongo/index';
import { UserTagsEnum } from '@fastgpt/global/support/user/type'; import { UserTagsSchema } from '@fastgpt/global/support/user/type';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
export const collectionName = 'app_templates'; export const collectionName = 'app_templates';
...@@ -23,11 +23,11 @@ const AppTemplateSchema = new Schema({ ...@@ -23,11 +23,11 @@ const AppTemplateSchema = new Schema({
isPromoted: Boolean, isPromoted: Boolean,
promoteTags: { promoteTags: {
type: [String], type: [String],
enum: UserTagsEnum.enum enum: UserTagsSchema.enum
}, },
hideTags: { hideTags: {
type: [String], type: [String],
enum: UserTagsEnum.enum enum: UserTagsSchema.enum
}, },
recommendText: String, recommendText: String,
userGuide: Object, userGuide: Object,
......
# FastGPT 系统工具设计
## 工具分类
1. 系统工具 (从 FastGPT-Plugin Service 获得的工具)
2. 系统工作流工具(商业版后台配置的工具,关联一个工作流)
3. 用户自定义的工具
1. mcp 工具
2. http 工具
3. 工作流工具(工作流,Agent 本身也可以被工具调用)
## 仓储层
## 展示层
import { RunToolWithStream } from '@fastgpt/global/sdk/fastgpt-plugin';
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
import { pluginClient, PLUGIN_BASE_URL, PLUGIN_TOKEN } from '../../../thirdProvider/fastgptPlugin';
import { retryFn } from '@fastgpt/global/common/system/utils';
export async function APIGetSystemToolList() {
const tools = await pluginClient.listTools();
return tools.map((item) => {
return {
...item,
id: `${AppToolSourceEnum.systemTool}-${item.toolId}`,
parentId: item.parentId ? `${AppToolSourceEnum.systemTool}-${item.parentId}` : undefined,
avatar: item.icon
};
});
}
const runToolInstance = new RunToolWithStream(PLUGIN_BASE_URL, PLUGIN_TOKEN);
export const APIRunSystemTool = runToolInstance.run.bind(runToolInstance);
export const getSystemToolTags = () => retryFn(async () => await pluginClient.getToolTags());
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,
lang = 'en',
source: toolSource = 'system'
}: {
pluginId: string;
versionId?: string;
lang?: localeType;
source?: string;
}): Promise<FlowNodeTemplateType> {
const systemToolRepo = SystemToolRepo.getInstance();
const toolDetail = await systemToolRepo.getSystemToolDetail({
pluginId,
version: versionId,
lang,
source: toolSource
});
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,
userGuide: toolDetail.userGuide ?? undefined,
showStatus: true,
isTool: true,
catchError: false,
version: versionId, // 为 undefined 时,为保持最新版
versionLabel: versionId,
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;
}
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MongoResourcePermission } from '../../../../support/permission/schema';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { AppTypeEnum, AppFolderTypeList } from '@fastgpt/global/core/app/constants';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { sumPer } from '@fastgpt/global/support/permission/utils';
import { getGroupsByTmbId } from '../../../../support/permission/memberGroup/controllers';
import { getOrgIdSetWithParentByTmbId } from '../../../../support/permission/org/controllers';
import { MongoApp } from '../../schema';
/**
* 获取用户级别的个人可用的工作流工具, 包括:
* mcp 工具, http 工具, 工作流工具
*/
export const getUserAvaliableWorkflowTools = async ({
teamId,
tmbId
}: {
teamId: string;
tmbId: string;
}) => {
// Get team all app permissions
const [roleList, myGroupMap, myOrgSet] = await Promise.all([
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: {
$exists: true
}
}).lean(),
getGroupsByTmbId({
tmbId,
teamId
}).then((item) => {
const map = new Map<string, 1>();
item.forEach((item) => {
map.set(String(item._id), 1);
});
return map;
}),
getOrgIdSetWithParentByTmbId({
teamId,
tmbId
})
]);
// Get my permissions
const myPerList = roleList.filter(
(item) =>
String(item.tmbId) === String(tmbId) ||
myGroupMap.has(String(item.groupId)) ||
myOrgSet.has(String(item.orgId))
);
const myApps: {
_id: string;
name: string;
intro?: string;
tmbId: string;
type: AppTypeEnum;
parentId?: ParentIdType;
inheritPermission?: boolean;
}[] = await MongoApp.find(
{ teamId, type: { $in: [AppTypeEnum.httpToolSet, AppTypeEnum.mcpToolSet] }, deleteTime: null },
'_id name intro tmbId type parentId inheritPermission'
).lean();
// Add app permission and filter apps by read permission
const formatApps = myApps
.map((app) => {
const { Per, privateApp } = (() => {
const getPer = (appId: string) => {
const tmbRole = myPerList.find(
(item) => String(item.resourceId) === appId && !!item.tmbId
)?.permission;
const groupAndOrgRole = sumPer(
...myPerList
.filter(
(item) => String(item.resourceId) === appId && (!!item.groupId || !!item.orgId)
)
.map((item) => item.permission)
);
return new AppPermission({
role: tmbRole ?? groupAndOrgRole,
isOwner: String(app.tmbId) === String(tmbId)
});
};
const getClbCount = (appId: string) => {
return roleList.filter((item) => String(item.resourceId) === String(appId)).length;
};
// Inherit app, check parent folder clb and it's own clb
if (!AppFolderTypeList.includes(app.type) && app.parentId && app.inheritPermission) {
return {
Per: getPer(String(app.parentId)).addRole(getPer(String(app._id)).role),
privateApp: getClbCount(String(app.parentId)) <= 1
};
}
return {
Per: getPer(String(app._id)),
privateApp: getClbCount(String(app._id)) <= 1
};
})();
return {
...app,
parentId: app.parentId,
permission: Per,
private: privateApp
};
})
.filter((app) => app.permission.hasReadPer);
return formatApps;
};
import type { localeType } from '@fastgpt/global/common/i18n/type'; import type { localeType } from '@fastgpt/global/common/i18n/type';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { getSystemToolsWithInstalled, getMyTools } from '../../../../app/tool/controller';
import type { ExecutionPlanType, TopAgentGenerationAnswerType } from './type'; import type { ExecutionPlanType, TopAgentGenerationAnswerType } from './type';
import { SubAppIds, systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants'; import { SubAppIds, systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants';
import { MongoDataset } from '../../../../dataset/schema'; import { MongoDataset } from '../../../../dataset/schema';
...@@ -9,6 +8,8 @@ import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant ...@@ -9,6 +8,8 @@ import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant
import { getGroupsByTmbId } from '../../../../../support/permission/memberGroup/controllers'; import { getGroupsByTmbId } from '../../../../../support/permission/memberGroup/controllers';
import { getOrgIdSetWithParentByTmbId } from '../../../../../support/permission/org/controllers'; import { getOrgIdSetWithParentByTmbId } from '../../../../../support/permission/org/controllers';
import { SANDBOX_SHELL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_SHELL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
import { getUserAvaliableWorkflowTools } from '../../../../app/tool/workflowTool';
import { SystemToolRepo } from '../../../../app/tool/systemTool/systemTool.repo';
const getAccessibleDatasets = async ({ teamId, tmbId }: { teamId: string; tmbId: string }) => { const getAccessibleDatasets = async ({ teamId, tmbId }: { teamId: string; tmbId: string }) => {
const [roleList, myGroupMap, myOrgSet] = await Promise.all([ const [roleList, myGroupMap, myOrgSet] = await Promise.all([
...@@ -71,31 +72,28 @@ ${dataset} ...@@ -71,31 +72,28 @@ ${dataset}
`; `;
}; };
const systemToolRepo = SystemToolRepo.getInstance();
const [systemTools, myTools, myDatasets] = await Promise.all([ const [systemTools, myTools, myDatasets] = await Promise.all([
getSystemToolsWithInstalled({ systemToolRepo
teamId, .getSystemToolList({
isRoot sources: [
}).then((res) => 'system'
res // teamId
.filter((tool) => { ],
return tool.installed && !tool.parentId; lang
}) })
.map((tool) => { .then((res) =>
res.map((tool) => {
const toolId = tool.id; const toolId = tool.id;
const name = const name = tool.name;
typeof tool.name === 'string' const intro = tool.intro;
? tool.name
: tool.name?.en || tool.name?.[lang] || '未命名';
const intro =
typeof tool.intro === 'string'
? tool.intro
: tool.intro?.en || tool.intro?.[lang] || '';
const description = tool.toolDescription || intro || '暂无描述'; const description = tool.toolDescription || intro || '暂无描述';
return `- **${toolId}** [工具]: ${name} - ${description}`; return `- **${toolId}** [工具]: ${name} - ${description}`;
}) })
), ),
getMyTools({ teamId, tmbId }).then((res) => getUserAvaliableWorkflowTools({ teamId, tmbId }).then((res) =>
res.map((tool) => { res.map((tool) => {
const toolId = tool._id; const toolId = tool._id;
return `- **${toolId}** [工具]: ${tool.name} - ${tool.intro}`; return `- **${toolId}** [工具]: ${tool.name} - ${tool.intro}`;
......
...@@ -6,6 +6,9 @@ const { Schema } = connectionMongo; ...@@ -6,6 +6,9 @@ const { Schema } = connectionMongo;
export const collectionName = 'team_installed_plugins'; export const collectionName = 'team_installed_plugins';
/**
* 暂时没用, 后续改造成团队层面的对插件的管理库
*/
const TeamInstalledPluginSchema = new Schema({ const TeamInstalledPluginSchema = new Schema({
teamId: { teamId: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
......
import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; import { connectionMongo, getMongoModel } from '../../../common/mongo/index';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type'; import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type';
import { UserTagsEnum } from '@fastgpt/global/support/user/type'; import type { PluginStatusType } from '@fastgpt/global/core/plugin/type';
import { UserTagsSchema } from '@fastgpt/global/support/user/type';
export const collectionName = 'system_plugin_tools'; export const collectionName = 'system_plugin_tools';
/** 职责:
* 1. 管理系统级别安装的插件的相关配置(价格等)
* 2. 管理系统级别配置的工作流工具
*/
const SystemToolSchema = new Schema({ const SystemToolSchema = new Schema({
/** 有前缀的, systemTool-xxx, commercial-xxx,包含子工具: systemTool-xxx/childId */
pluginId: { pluginId: {
// commercial-id
type: String, type: String,
required: true required: true
}, },
/** 插件状态,默认为激活状态 */
status: { status: {
type: Number, type: Number,
default: 1 set(val: PluginStatusType | number) {
if (typeof val === 'number') return val;
switch (val) {
case 'Normal':
return 1;
case 'SoonOffline':
return 2;
case 'Offline':
return 3;
default:
return 1;
}
}, },
defaultInstalled: { get(val: number) {
type: Boolean, switch (val) {
default: false case 1:
return 'Normal';
case 2:
return 'SoonOffline';
case 3:
return 'Offline';
default:
return 'Normal';
}
}
}, },
/**
* 插件的原始费用(展示用,现在没用)
*/
originCost: { originCost: {
type: Number, type: Number,
default: 0 default: 0
}, },
/** 当前价格,实际生效 */
currentCost: { currentCost: {
type: Number, type: Number,
default: 0 default: 0
}, },
/** 是否收取系统密钥费用 */
hasTokenFee: { hasTokenFee: {
type: Boolean, type: Boolean,
default: false default: false
}, },
/** 排序 */
pluginOrder: { pluginOrder: {
type: Number type: Number
}, },
/** 系统密钥价格 */
systemKeyCost: { systemKeyCost: {
type: Number, type: Number,
default: 0 default: 0
}, },
/**
* 系统配置的工作流工具的相关配置
*/
customConfig: Object, customConfig: Object,
/**
* 系统密钥的值
*/
secretsVal: Object,
/** @deprecated */
inputListVal: Object, inputListVal: Object,
/**
* 推荐 Tags,有对应 tag 的用户看到是推荐状态
*/
promoteTags: { promoteTags: {
type: [String], type: [String],
enum: UserTagsEnum.enum enum: UserTagsSchema.enum
}, },
/**
* 隐藏 Tags,有对应 tag 的用户看不到
*/
hideTags: { hideTags: {
type: [String], type: [String],
enum: UserTagsEnum.enum enum: UserTagsSchema.enum
}, },
/** @deprecated */ /** @deprecated */
inputConfig: Array, inputConfig: Array,
/** @deprecated */ /** @deprecated */
isActive: Boolean isActive: {
type: Boolean,
required: false,
get() {
return true;
}
}
}); });
SystemToolSchema.index({ pluginId: 1 }); SystemToolSchema.index({ pluginId: 1 });
......
...@@ -206,6 +206,8 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...@@ -206,6 +206,8 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
useAgentSandbox: !!sandboxClient useAgentSandbox: !!sandboxClient
} }
); );
console.log('agentSubAppsMap', agentSubAppsMap);
// runtime 运行详情和工具卡需要根据 function name 反查展示名、头像和描述。 // runtime 运行详情和工具卡需要根据 function name 反查展示名、头像和描述。
// 用户工具与系统工具的 id 形态不完全一致,这里统一归一化查询。 // 用户工具与系统工具的 id 形态不完全一致,这里统一归一化查询。
const getSubAppInfo = (id: string) => { const getSubAppInfo = (id: string) => {
......
...@@ -181,12 +181,11 @@ export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppRespon ...@@ -181,12 +181,11 @@ export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppRespon
responseChatItemId: data.responseChatItemId, responseChatItemId: data.responseChatItemId,
histories: [], histories: [],
uid: data.uid, uid: data.uid,
variablesConfig: chatConfig.variables, variablesConfig: [],
inputVariables: customAppVariables, inputVariables: {},
externalVariables: externalProvider?.externalWorkflowVariables, externalVariables: externalProvider?.externalWorkflowVariables,
sourceVariableState: variableState sourceVariableState: variableState
}); });
const childrenRunVariables = childrenVariableState.toRuntimeRecord();
const runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)).map( const runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)).map(
(node) => { (node) => {
// Update plugin input value // Update plugin input value
...@@ -195,15 +194,15 @@ export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppRespon ...@@ -195,15 +194,15 @@ export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppRespon
...node, ...node,
showStatus: false, showStatus: false,
inputs: node.inputs.map((input) => { inputs: node.inputs.map((input) => {
let val = childrenRunVariables[input.key] ?? input.value; let val = customAppVariables[input.key] ?? input.value;
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) { if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) {
val = anyValueDecrypt(val); val = anyValueDecrypt(val);
} else if ( } else if (
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) &&
Array.isArray(val) && Array.isArray(val) &&
childrenRunVariables[input.key] customAppVariables[input.key]
) { ) {
childrenRunVariables[input.key] = val.map((item) => customAppVariables[input.key] = val.map((item) =>
typeof item === 'string' ? item : item.url typeof item === 'string' ? item : item.url
); );
} }
...@@ -249,7 +248,7 @@ export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppRespon ...@@ -249,7 +248,7 @@ export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppRespon
variableState: childrenVariableState, variableState: childrenVariableState,
query: serverGetWorkflowToolRunUserQuery({ query: serverGetWorkflowToolRunUserQuery({
pluginInputs: getWorkflowToolInputsFromStoreNodes(nodes), pluginInputs: getWorkflowToolInputsFromStoreNodes(nodes),
variables: childrenRunVariables variables: customAppVariables
}).value, }).value,
stream: false, stream: false,
workflowStreamResponse: undefined workflowStreamResponse: undefined
......
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants';
import type { DispatchSubAppResponse } from '../../type'; import type { DispatchSubAppResponse } from '../../type';
import { getSystemToolById } from '../../../../../../app/tool/controller'; import { getToolRawId } from '@fastgpt/global/core/app/tool/utils';
import { getSecretValue } from '../../../../../../../common/secret/utils'; import { getSecretValue } from '../../../../../../../common/secret/utils';
import { MongoSystemTool } from '../../../../../../plugin/tool/systemToolSchema';
import { APIRunSystemTool } from '../../../../../../app/tool/api';
import type { import type {
ChatDispatchProps, ChatDispatchProps,
RuntimeNodeItemType RuntimeNodeItemType
...@@ -18,11 +16,12 @@ import { getErrText } from '@fastgpt/global/common/error/utils'; ...@@ -18,11 +16,12 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { getAppVersionById } from '../../../../../../app/version/controller'; import { getAppVersionById } from '../../../../../../app/version/controller';
import { assertMCPUrlNotInternal, MCPClient } from '../../../../../../app/mcp'; import { assertMCPUrlNotInternal, MCPClient } from '../../../../../../app/mcp';
import { runHTTPTool } from '../../../../../../app/http'; import { runHTTPTool } from '../../../../../../app/http';
import { getS3ChatSource } from '../../../../../../../common/s3/sources/chat';
import { parseToolId } from '../../../../child/runTool'; import { parseToolId } from '../../../../child/runTool';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { pluginClient } from '../../../../../../../thirdProvider/fastgptPlugin';
import { SystemToolRepo } from '../../../../../../app/tool/systemTool/systemTool.repo';
import { InvokeProcessor } from '../../../../../../../support/invoke/invoke';
type SystemInputConfigType = { type SystemInputConfigType = {
type: SystemToolSecretInputTypeEnum; type: SystemToolSecretInputTypeEnum;
...@@ -84,7 +83,12 @@ export const dispatchTool = async ({ ...@@ -84,7 +83,12 @@ export const dispatchTool = async ({
try { try {
if (toolConfig?.systemTool?.toolId) { if (toolConfig?.systemTool?.toolId) {
const tool = await getSystemToolById(toolConfig?.systemTool.toolId); const systemToolRepo = SystemToolRepo.getInstance();
const tool = await systemToolRepo.getSystemToolRuntime({
pluginId: toolConfig.systemTool.toolId,
source: 'system',
version
});
const inputConfigParams = await (async () => { const inputConfigParams = await (async () => {
switch (system_input_config?.type) { switch (system_input_config?.type) {
case SystemToolSecretInputTypeEnum.team: case SystemToolSecretInputTypeEnum.team:
...@@ -95,48 +99,42 @@ export const dispatchTool = async ({ ...@@ -95,48 +99,42 @@ export const dispatchTool = async ({
}); });
case SystemToolSecretInputTypeEnum.system: case SystemToolSecretInputTypeEnum.system:
default: default:
// read from mongo return tool.secretsVal ?? {};
const dbPlugin = await MongoSystemTool.findOne({
pluginId: tool.id
}).lean();
return dbPlugin?.inputListVal || {};
} }
})(); })();
const inputs = {
...Object.fromEntries(Object.entries(params)),
...inputConfigParams
};
const formatToolId = tool.id.split('-')[1]; const formatToolId = getToolRawId(tool.id);
let answerText = ''; let answerText = '';
const res = await APIRunSystemTool({ const invokeToken = new InvokeProcessor({
toolId: formatToolId, appId: runningAppInfo.id,
inputs, chatId,
uId: uid,
permissions: tool.permissions ?? [],
teamId: runningAppInfo.teamId,
tmbId: runningAppInfo.tmbId
}).generateToken();
const childId = toolConfig.systemTool.toolId.split('/')[1];
const res = await pluginClient.runToolStream({
pluginId: formatToolId,
...(childId ? { childId } : {}),
version: tool.version ?? version ?? '',
source: 'system', // TODO: 后续 source 需要从节点配置中获取到
input: Object.fromEntries(Object.entries(params)),
secrets: inputConfigParams,
systemVar: { systemVar: {
user: {
id: uid,
username: runningUserInfo.username,
contact: runningUserInfo.contact,
membername: runningUserInfo.memberName,
teamName: runningUserInfo.teamName,
teamId: runningUserInfo.teamId,
name: runningUserInfo.tmbId
},
app: { app: {
id: runningAppInfo.id, id: runningAppInfo.id,
name: runningAppInfo.id name: runningAppInfo.id
}, },
tool: { chat: {
id: formatToolId,
version: version || tool.versionList?.[0]?.value || '',
prefix: getS3ChatSource().getToolFilePrefix({
appId: runningAppInfo.id,
chatId, chatId,
uId: uid uid
})
}, },
time: String(variableState.get('cTime') ?? '') time: String(variableState.get('cTime') ?? ''),
invokeToken
}, },
onMessage: ({ type, content }) => { onMessage: ({ type, content }) => {
if (workflowStreamResponse && content) { if (workflowStreamResponse && content) {
...@@ -151,7 +149,7 @@ export const dispatchTool = async ({ ...@@ -151,7 +149,7 @@ export const dispatchTool = async ({
} }
}); });
let result = res.output || {}; const result: any = res.output || {};
if (res.error) { if (res.error) {
return getErrResponse(res.error); return getErrResponse(res.error);
......
...@@ -24,6 +24,7 @@ import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/ ...@@ -24,6 +24,7 @@ import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/
import type { SubAppInitType } from '../type'; import type { SubAppInitType } from '../type';
import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils'; import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils';
import { getLogger, LogCategories } from '../../../../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../../../common/logger';
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
export const getAgentRuntimeTools = async ({ export const getAgentRuntimeTools = async ({
tools, tools,
...@@ -110,7 +111,7 @@ export const getAgentRuntimeTools = async ({ ...@@ -110,7 +111,7 @@ export const getAgentRuntimeTools = async ({
return Promise.all( return Promise.all(
tools.map<Promise<SubAppInitType[]>>(async (tool) => { tools.map<Promise<SubAppInitType[]>>(async (tool) => {
try { try {
const { pluginId, authAppId } = splitCombineToolId(tool.id); const { pluginId, authAppId, source } = splitCombineToolId(tool.id);
const [toolNode] = await Promise.all([ const [toolNode] = await Promise.all([
getChildAppPreviewNode({ getChildAppPreviewNode({
...@@ -127,8 +128,6 @@ export const getAgentRuntimeTools = async ({ ...@@ -127,8 +128,6 @@ export const getAgentRuntimeTools = async ({
] ]
: []) : [])
]); ]);
// console.log('toolNode', toolNode)
// Check if tool configuration is complete
// 1. Add config value to toolNode.inputs // 1. Add config value to toolNode.inputs
toolNode.inputs.forEach((input) => { toolNode.inputs.forEach((input) => {
const value = tool.config[input.key]; const value = tool.config[input.key];
...@@ -149,6 +148,9 @@ export const getAgentRuntimeTools = async ({ ...@@ -149,6 +148,9 @@ export const getAgentRuntimeTools = async ({
} }
const toolType = (() => { const toolType = (() => {
if (source === AppToolSourceEnum.commercial) {
return 'commercialTool';
}
if (toolNode.flowNodeType === FlowNodeTypeEnum.appModule) { if (toolNode.flowNodeType === FlowNodeTypeEnum.appModule) {
return 'workflow'; return 'workflow';
} }
...@@ -260,7 +262,17 @@ export const getAgentRuntimeTools = async ({ ...@@ -260,7 +262,17 @@ export const getAgentRuntimeTools = async ({
} }
return []; return [];
} else { }
// else if (source === AppToolSourceEnum.commercial) {
// const systemToolRepo = SystemToolRepo.getInstance();
// const detail = await systemToolRepo.getSystemToolDetail({
// pluginId
// })
// }
else {
const cleanedPluginId = pluginId.replace(/[^a-zA-Z0-9_-]/g, ''); const cleanedPluginId = pluginId.replace(/[^a-zA-Z0-9_-]/g, '');
return [ return [
......
...@@ -17,7 +17,7 @@ export type DispatchSubAppResponse = { ...@@ -17,7 +17,7 @@ export type DispatchSubAppResponse = {
}; };
export const SubAppRuntimeSchema = z.object({ export const SubAppRuntimeSchema = z.object({
type: z.enum(['tool', 'workflow', 'toolWorkflow']), type: z.enum(['tool', 'workflow', 'toolWorkflow', 'commercialTool']),
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
avatar: z.string().optional(), avatar: z.string().optional(),
......
...@@ -19,6 +19,7 @@ import { dispatchTool } from './sub/tool'; ...@@ -19,6 +19,7 @@ import { dispatchTool } from './sub/tool';
import type { WorkflowResponseItemType } from '../../type'; import type { WorkflowResponseItemType } from '../../type';
import { dispatchApp, dispatchPlugin } from './sub/app'; import { dispatchApp, dispatchPlugin } from './sub/app';
import type { SandboxClient } from '../../../../ai/sandbox/service/runtime'; import type { SandboxClient } from '../../../../ai/sandbox/service/runtime';
import { SystemToolRepo } from '../../../../app/tool/systemTool/systemTool.repo';
/** /**
* 收集 Agent 节点可用的系统工具和用户选择的子应用工具。 * 收集 Agent 节点可用的系统工具和用户选择的子应用工具。
...@@ -69,6 +70,8 @@ export const getSubapps = async ({ ...@@ -69,6 +70,8 @@ export const getSubapps = async ({
tmbId, tmbId,
lang lang
}); });
console.log('tools', JSON.stringify(tools, null, 2));
formatTools.forEach((tool) => { formatTools.forEach((tool) => {
completionTools.push(tool.requestSchema); completionTools.push(tool.requestSchema);
subAppsMap.set(tool.id, { subAppsMap.set(tool.id, {
...@@ -303,12 +306,28 @@ export const getExecuteTool = ({ ...@@ -303,12 +306,28 @@ export const getExecuteTool = ({
usages, usages,
nodeResponse nodeResponse
}; };
} else if (tool.type === 'toolWorkflow') { } else if (tool.type === 'toolWorkflow' || tool.type === 'commercialTool') {
const id = await (async () => {
if (tool.type === 'toolWorkflow') {
return tool.id;
} else {
const systemToolRepo = SystemToolRepo.getInstance();
const trueId = (
await systemToolRepo.getSystemToolDetail({
pluginId: `commercial-${tool.id}`
})
).associatedPluginId;
if (!trueId) {
throw new Error('No associated plugin found');
}
return trueId;
}
})();
const { response, usages, nodeResponse } = await dispatchPlugin({ const { response, usages, nodeResponse } = await dispatchPlugin({
app: { app: {
name: tool.name, name: tool.name,
avatar: tool.avatar, avatar: tool.avatar,
id: tool.id id
}, },
userChatInput: '', userChatInput: '',
customAppVariables: requestParams, customAppVariables: requestParams,
......
...@@ -11,18 +11,18 @@ import { assertMCPUrlNotInternal, MCPClient } from '../../../app/mcp'; ...@@ -11,18 +11,18 @@ import { assertMCPUrlNotInternal, MCPClient } from '../../../app/mcp';
import { getSecretValue } from '../../../../common/secret/utils'; import { getSecretValue } from '../../../../common/secret/utils';
import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/type'; import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/type';
import { APIRunSystemTool } from '../../../app/tool/api';
import { MongoSystemTool } from '../../../plugin/tool/systemToolSchema';
import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants';
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import { getSystemToolById } from '../../../app/tool/controller';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import { pushTrack } from '../../../../common/middle/tracks/utils'; import { pushTrack } from '../../../../common/middle/tracks/utils';
import { getNodeErrResponse } from '../utils'; import { getNodeErrResponse } from '../utils';
import { getAppVersionById } from '../../../../core/app/version/controller'; import { getAppVersionById } from '../../../../core/app/version/controller';
import { runHTTPTool } from '../../../app/http'; import { runHTTPTool } from '../../../app/http';
import { getS3ChatSource } from '../../../../common/s3/sources/chat';
import { getWorkflowContext } from '../../utils/context'; import { getWorkflowContext } from '../../utils/context';
import { getToolRawId } from '@fastgpt/global/core/app/tool/utils';
import { pluginClient } from '../../../../thirdProvider/fastgptPlugin';
import { SystemToolRepo } from '../../../app/tool/systemTool/systemTool.repo';
import { InvokeProcessor } from '../../../../support/invoke/invoke';
type SystemInputConfigType = { type SystemInputConfigType = {
type: SystemToolSecretInputTypeEnum; type: SystemToolSecretInputTypeEnum;
...@@ -66,7 +66,12 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -66,7 +66,12 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
try { try {
// run system tool // run system tool
if (toolConfig?.systemTool?.toolId) { if (toolConfig?.systemTool?.toolId) {
const tool = await getSystemToolById(toolConfig.systemTool!.toolId); const systemToolRepo = SystemToolRepo.getInstance();
const tool = await systemToolRepo.getSystemToolRuntime({
pluginId: toolConfig.systemTool.toolId,
source: 'system', // TODO : 后续用户调用时传 teamId
version
});
const inputConfigParams = await (async () => { const inputConfigParams = await (async () => {
switch (params.system_input_config?.type) { switch (params.system_input_config?.type) {
...@@ -79,46 +84,43 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -79,46 +84,43 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
}); });
case SystemToolSecretInputTypeEnum.system: case SystemToolSecretInputTypeEnum.system:
default: default:
// read from mongo return tool.secretsVal ?? {};
const dbPlugin = await MongoSystemTool.findOne({
pluginId: toolConfig.systemTool?.toolId
}).lean();
return dbPlugin?.inputListVal || {};
} }
})(); })();
toolInput = Object.fromEntries( toolInput = Object.fromEntries(
Object.entries(params).filter(([key]) => key !== NodeInputKeyEnum.systemInputConfig) Object.entries(params).filter(([key]) => key !== NodeInputKeyEnum.systemInputConfig)
); );
const inputs = {
...toolInput,
...inputConfigParams
};
const formatToolId = tool.id.split('-')[1]; const invokeToken = new InvokeProcessor({
appId,
chatId,
uId,
teamId: String(runningUserInfo.teamId),
tmbId: String(runningUserInfo.tmbId),
permissions: tool.permissions ?? []
}).generateToken();
const formatToolId = getToolRawId(toolConfig.systemTool!.toolId);
const childId = toolConfig.systemTool.toolId.split('/')[1];
let answerText = ''; let answerText = '';
const res = await APIRunSystemTool({ const res = await pluginClient.runToolStream({
toolId: formatToolId, pluginId: formatToolId,
inputs, version: tool.version ?? version ?? '',
source: 'system', // TODO: 后续用户调用时传 teamId
input: toolInput,
secrets: inputConfigParams,
...(childId ? { childId } : {}),
systemVar: { systemVar: {
user: {
id: props.uid,
username: runningUserInfo.username,
contact: runningUserInfo.contact,
membername: runningUserInfo.memberName,
teamName: runningUserInfo.teamName,
teamId: runningUserInfo.teamId,
name: runningUserInfo.tmbId
},
app: { app: {
id: runningAppInfo.id, id: runningAppInfo.id,
name: runningAppInfo.id name: runningAppInfo.name
}, },
tool: { chat: {
id: formatToolId, chatId,
version: version || tool.versionList?.[0]?.value || '', uid: uId
prefix: getS3ChatSource().getToolFilePrefix({ appId, chatId, uId })
}, },
invokeToken,
time: cTime time: cTime
}, },
onMessage: ({ type, content }) => { onMessage: ({ type, content }) => {
...@@ -134,11 +136,11 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -134,11 +136,11 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
} }
}); });
let result = res.output || {}; const result = (res.output as any) || {};
if (res.error) { if (res.error) {
// 适配旧版:旧版本没有catchError,部分工具会正常返回 error 字段作为响应。 // 适配旧版:旧版本没有catchError,部分工具会正常返回 error 字段作为响应。
if (catchError === undefined && typeof res.error === 'object') { if (catchError === undefined && typeof res.error === 'object' && 'error' in res.error) {
return { return {
data: res.error, data: res.error,
[DispatchNodeResponseKeyEnum.nodeResponse]: { [DispatchNodeResponseKeyEnum.nodeResponse]: {
...@@ -150,21 +152,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -150,21 +152,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
}; };
} }
// String error(Common error, not custom) throw res.error;
if (typeof res.error === 'string') {
throw new Error(res.error);
}
// Custom error field
return {
error: res.error,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
error: res.error,
moduleLogo: avatar
},
[DispatchNodeResponseKeyEnum.toolResponses]: res.error
};
} }
const usagePoints = (() => { const usagePoints = (() => {
...@@ -190,7 +178,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -190,7 +178,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
toolId: tool.id, toolId: tool.id,
result: 1, result: 1,
usagePoint: usagePoints, usagePoint: usagePoints,
msg: result[NodeOutputKeyEnum.systemError] msg: String(res.error || '')
}); });
return { return {
...@@ -332,7 +320,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo ...@@ -332,7 +320,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
pushTrack.runSystemTool({ pushTrack.runSystemTool({
teamId: runningUserInfo.teamId, teamId: runningUserInfo.teamId,
tmbId: runningUserInfo.tmbId, tmbId: runningUserInfo.tmbId,
uid: runningUserInfo.tmbId, uid: uId,
toolId: systemToolId, toolId: systemToolId,
result: 0, result: 0,
msg: getErrText(error) msg: getErrText(error)
......
...@@ -30,8 +30,8 @@ import type { AppToolRuntimeType } from '@fastgpt/global/core/app/tool/type'; ...@@ -30,8 +30,8 @@ import type { AppToolRuntimeType } from '@fastgpt/global/core/app/tool/type';
import { anyValueDecrypt } from '../../../../common/secret/utils'; import { anyValueDecrypt } from '../../../../common/secret/utils';
import { getAppVersionById } from '../../../app/version/controller'; import { getAppVersionById } from '../../../app/version/controller';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { getSystemToolByIdAndVersionId } from '../../../app/tool/controller';
import { WorkflowVariableState } from '../utils/variables'; import { WorkflowVariableState } from '../utils/variables';
import { SystemToolRepo } from '../../../app/tool/systemTool/systemTool.repo';
type RunPluginProps = ModuleDispatchProps<{ type RunPluginProps = ModuleDispatchProps<{
[NodeInputKeyEnum.forbidStream]?: boolean; [NodeInputKeyEnum.forbidStream]?: boolean;
...@@ -46,6 +46,12 @@ type RunPluginResponse = DispatchNodeResultType< ...@@ -46,6 +46,12 @@ type RunPluginResponse = DispatchNodeResultType<
} }
>; >;
/**
* 工作流插件处理函数
* 1. 系统工具 systemTool- 转发到 dispatchRunTool (为了兼容旧的数据)
* 2. personal (自己的插件)
* 3. commercial (系统级别的工作流插件)
*/
export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPluginResponse> => { export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPluginResponse> => {
const { const {
node: { pluginId, version }, node: { pluginId, version },
...@@ -80,7 +86,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi ...@@ -80,7 +86,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
return getNodeErrResponse({ error: 'pluginId can not find' }); return getNodeErrResponse({ error: 'pluginId can not find' });
} }
/* /**
1. Team app (personal): 走 team 权限校验 1. Team app (personal): 走 team 权限校验
2. Admin selected system tool (commercial): 系统级工具,不做用户态权限校验 2. Admin selected system tool (commercial): 系统级工具,不做用户态权限校验
*/ */
...@@ -117,11 +123,15 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi ...@@ -117,11 +123,15 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
hasTokenFee: false hasTokenFee: false
}; };
} else { } else {
const systemToolRepo = SystemToolRepo.getInstance();
// commercial: 通过系统工具加载(内部会解析 associatedPluginId 对应的 app 版本) // commercial: 通过系统工具加载(内部会解析 associatedPluginId 对应的 app 版本)
const systemTool = await getSystemToolByIdAndVersionId(pluginId, version); const systemTool = await systemToolRepo.getSystemToolWorkflowRuntime({
pluginId,
version
});
workflowTool = { workflowTool = {
id: systemTool.id, id: pluginId,
teamId: systemTool.teamId, teamId: systemTool.teamId,
tmbId: systemTool.tmbId, tmbId: systemTool.tmbId,
name: parseI18nString(systemTool.name, props.lang), name: parseI18nString(systemTool.name, props.lang),
...@@ -129,8 +139,8 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi ...@@ -129,8 +139,8 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
showStatus: true, showStatus: true,
currentCost: systemTool.currentCost ?? 0, currentCost: systemTool.currentCost ?? 0,
systemKeyCost: systemTool.systemKeyCost ?? 0, systemKeyCost: systemTool.systemKeyCost ?? 0,
nodes: systemTool.workflow.nodes, nodes: systemTool.nodes,
edges: systemTool.workflow.edges, edges: systemTool.edges,
hasTokenFee: !!systemTool.hasTokenFee hasTokenFee: !!systemTool.hasTokenFee
}; };
} }
......
import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { countPromptTokensBatch } from '../../../common/string/tiktoken/index'; import { countPromptTokensBatch } from '../../../common/string/tiktoken/index';
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { getSystemToolByIdAndVersionId, getSystemTools } from '../../app/tool/controller';
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';
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 { SystemToolRepo } from '../../app/tool/systemTool/systemTool.repo';
/* filter search result */ /* filter search result */
export const filterSearchResultsByMaxChars = async ( export const filterSearchResultsByMaxChars = async (
...@@ -31,54 +30,73 @@ export const filterSearchResultsByMaxChars = async ( ...@@ -31,54 +30,73 @@ export const filterSearchResultsByMaxChars = async (
return results.length === 0 ? list.slice(0, 1) : results; return results.length === 0 ? list.slice(0, 1) : results;
}; };
/**
* 把 SystemTool Toolset 替换为 Tool 节点
*/
export async function getSystemToolRunTimeNodeFromSystemToolset({ export async function getSystemToolRunTimeNodeFromSystemToolset({
toolSetNode, toolSetNode,
lang = 'en' lang = 'en'
}: { }: {
toolSetNode: Pick<RuntimeNodeItemType, 'toolConfig' | 'inputs' | 'nodeId'>; toolSetNode: Pick<RuntimeNodeItemType, 'toolConfig' | 'inputs' | 'nodeId' | 'version'>;
lang?: localeType; lang?: localeType;
}): Promise<RuntimeNodeItemType[]> { }): Promise<RuntimeNodeItemType[]> {
const systemToolId = toolSetNode.toolConfig?.systemToolSet?.toolId!; const systemToolId = toolSetNode.toolConfig?.systemToolSet?.toolId!;
const selectedTools = toolSetNode.toolConfig?.systemToolSet?.toolList ?? [];
if (!selectedTools.length) return [];
const toolsetInputConfig = toolSetNode.inputs.find( const toolsetInputConfig = toolSetNode.inputs.find(
(item) => item.key === NodeInputKeyEnum.systemInputConfig (item) => item.key === NodeInputKeyEnum.systemInputConfig
); );
const tools = await getSystemTools(); const systemToolRepo = SystemToolRepo.getInstance();
const children = tools.filter( const tool = await systemToolRepo.getSystemToolDetail({
(item) => item.parentId === systemToolId && (item.status === 1 || item.status === undefined) pluginId: systemToolId,
); lang,
const nodes = await Promise.all( // source: toolSetNode.toolConfig?.systemToolSet?.source,
children.map(async (child, index) => { source: 'system',
const toolListItem = toolSetNode.toolConfig?.systemToolSet?.toolList.find( version: toolSetNode.version,
(item) => item.toolId === child.id fallbackLatestVersion: true
});
if (!tool.children) return [];
const runtimeVersion = tool.version || toolSetNode.version;
const childMap = new Map<string, (typeof tool.children)[number]>(
tool.children.map((child) => [`${systemToolId}/${child.id}`, child])
); );
tool.children.forEach((child) => {
childMap.set(child.id, child);
});
const tool = await getSystemToolByIdAndVersionId(child.id); const nodes = selectedTools.flatMap((selectedTool) => {
const child = childMap.get(selectedTool.toolId);
if (!child) return [];
const inputs = tool.inputs ?? []; const pluginId = `${systemToolId}/${child.id}`;
if (toolsetInputConfig?.value) { const intro = selectedTool.description || child.description;
const configInput = inputs.find((item) => item.key === NodeInputKeyEnum.systemInputConfig); const toolDescription = selectedTool.description || child.toolDescription || child.description;
if (configInput) {
configInput.value = toolsetInputConfig.value;
}
}
return { return {
...tool,
inputs,
outputs: tool.outputs ?? [],
name: toolListItem?.name || parseI18nString(tool.name, lang),
intro: toolListItem?.description || parseI18nString(tool.intro, lang),
flowNodeType: FlowNodeTypeEnum.tool, flowNodeType: FlowNodeTypeEnum.tool,
nodeId: `${toolSetNode.nodeId}${index}`, avatar: tool.avatar,
inputs: toolsetInputConfig
? [toolsetInputConfig, ...(child.inputs ?? [])]
: (child.inputs ?? []),
outputs: child.outputs ?? [],
name: selectedTool.name || child.name,
intro,
nodeId: `${toolSetNode.nodeId}${child.id}`,
version: runtimeVersion,
toolDescription,
toolConfig: { toolConfig: {
systemTool: { systemTool: {
toolId: child.id toolId: pluginId
}
} }
}; },
}) pluginId
); // BUG: 不知道 catchError 从哪里拿,后续需要优化实现
// catchError: toolSetNode.
} satisfies RuntimeNodeItemType;
});
return nodes; return nodes;
} }
...@@ -298,7 +298,10 @@ export const serviceEnv = createEnv({ ...@@ -298,7 +298,10 @@ export const serviceEnv = createEnv({
FEISHU_BASE_URL: UrlSchema.default('https://open.feishu.cn'), FEISHU_BASE_URL: UrlSchema.default('https://open.feishu.cn'),
DINGTALK_BASE_URL: UrlSchema.default('https://api.dingtalk.com'), DINGTALK_BASE_URL: UrlSchema.default('https://api.dingtalk.com'),
DINGTALK_OAPI_BASE_URL: UrlSchema.default('https://oapi.dingtalk.com'), DINGTALK_OAPI_BASE_URL: UrlSchema.default('https://oapi.dingtalk.com'),
YUQUE_DATASET_BASE_URL: UrlSchema.default('https://www.yuque.com') YUQUE_DATASET_BASE_URL: UrlSchema.default('https://www.yuque.com'),
// Invoke 反向调用相关
INVOKE_TOKEN_SECRET: z.string().default('token')
}, },
emptyStringAsUndefined: true, emptyStringAsUndefined: true,
runtimeEnv: process.env, runtimeEnv: process.env,
......
# FastGPT 反向调用处理器设计
**反向调用** 是指 FastGPT 提供一系列接口供 FastGPT-Plugin 调用以进行文件上传、模型调用、知识库检索等操作。
## 反向调用流程
### 1. 插件声明权限
在插件开发过程中声明权限,在插件安装解析时、插件市场中显示权限,确认安装则视为授权。
### 2. FastGPT 签发 InvokeToken
Payload: 权限、必要信息, 过期时间 30 分钟
### 3. FastGPT-Plugin 发送 invoke 请求,携带 token
### 4. FastGPT 处理 invoke 请求,返回结果。
## TODO
- [ ] 插件权限展示相关逻辑
import jwt from 'jsonwebtoken';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import {
PluginPermissionEnum,
type PluginPermissionEnumType
} from '@fastgpt/global/sdk/fastgpt-plugin';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import type { InvokeUserInfoResponseType } from '@fastgpt/global/openapi/plugin/invoke';
import { getS3ChatSource } from '../../common/s3/sources/chat';
import { serviceEnv } from '../../env';
import { getGroupsByTmbId } from '../permission/memberGroup/controllers';
import { getOrgsByTmbId } from '../permission/org/controllers';
import { MongoOrgModel } from '../permission/org/orgSchema';
import { getUserDetail } from '../user/controller';
import { MongoTeam } from '../user/team/teamSchema';
import { InvokeFileUploadSchema, InvokeSessionSchema, type InvokeFileUploadType } from './type';
import type { InvokeSessionType } from './type';
const INVOKE_TOKEN_EXPIRES_IN = 60 * 60;
/** 反向调用处理器 */
export class InvokeProcessor {
private _session: InvokeSessionType;
static jwtSecret = serviceEnv.INVOKE_TOKEN_SECRET;
public get session(): InvokeSessionType {
return this.session;
}
constructor(options: InvokeSessionType) {
this._session = options;
}
generateToken(): string {
const session = InvokeSessionSchema.parse(this._session);
return jwt.sign(session, InvokeProcessor.jwtSecret, {
expiresIn: INVOKE_TOKEN_EXPIRES_IN
});
}
static getInstanceFromToken(token?: string): InvokeProcessor {
if (!token) {
throw ERROR_ENUM.unAuthorization;
}
try {
const payload = jwt.verify(token, this.jwtSecret);
const session = InvokeSessionSchema.parse(payload);
return new InvokeProcessor(session);
} catch (error) {
throw ERROR_ENUM.unAuthorization;
}
}
private assertPermission(permission: PluginPermissionEnumType) {
const { permissions } = InvokeSessionSchema.parse(this._session);
if (!permissions.includes(permission)) {
throw ERROR_ENUM.unAuthorization;
}
}
getSessionWithPermission(permission: PluginPermissionEnumType): InvokeSessionType {
this.assertPermission(permission);
return InvokeSessionSchema.parse(this._session);
}
async handleFileUpload(params: InvokeFileUploadType): Promise<{ url: string }> {
this.assertPermission(PluginPermissionEnum['file-upload:allow']);
const { appId, chatId, uId } = InvokeSessionSchema.parse(this._session);
const { filename, body, contentType, expiredTime } = InvokeFileUploadSchema.parse(params);
const result = await getS3ChatSource().uploadChatFile({
appId,
chatId,
uId,
filename,
body,
contentType,
expiredTime
});
return {
url: result.accessUrl.url
};
}
async handleGetUserInfo(): Promise<InvokeUserInfoResponseType> {
this.assertPermission(PluginPermissionEnum['userInfo:read']);
const { tmbId, teamId } = InvokeSessionSchema.parse(this._session);
const [user, orgs, groups, team] = await Promise.all([
getUserDetail({ tmbId }),
getOrgsByTmbId({ teamId, tmbId }),
getGroupsByTmbId({ tmbId, teamId }),
MongoTeam.findById(teamId, {
name: 1
}).lean()
]);
if (!team) throw new Error('Team not found');
const orgInfos = orgs.length
? await MongoOrgModel.find(
{
_id: {
$in: orgs.map((org) => org.orgId)
}
},
{
name: 1,
pathId: 1
}
).lean()
: [];
return {
username: user.username,
memberName: user.team.memberName,
contact: user.contact,
orgs: orgInfos.map((org) => ({
name: org.name,
pathId: org.pathId
})),
groups: groups.map((group) => ({
name: group.name === DefaultGroupName ? team.name : group.name
}))
};
}
}
import z from 'zod';
import { UploadFileByBodySchema } from '../../common/s3/contracts/type';
import { PluginPermissionListSchema } from '@fastgpt/global/sdk/fastgpt-plugin';
export const InvokeSessionSchema = z.object({
appId: z.string().nonempty(),
chatId: z.string().nonempty(),
uId: z.string().nonempty(),
teamId: z.string().nonempty(),
tmbId: z.string().nonempty(),
permissions: PluginPermissionListSchema.default([])
});
export type InvokeSessionType = z.infer<typeof InvokeSessionSchema>;
export const InvokeFileUploadSchema = z.object({
filename: UploadFileByBodySchema.shape.filename,
body: UploadFileByBodySchema.shape.body,
contentType: UploadFileByBodySchema.shape.contentType,
expiredTime: UploadFileByBodySchema.shape.expiredTime
});
export type InvokeFileUploadType = z.infer<typeof InvokeFileUploadSchema>;
import { PLUGIN_TOKEN } from '../../../thirdProvider/fastgptPlugin/index';
import type { ApiRequestProps } from '../../../type/next';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
/**
* Auth plugin token from request header
* Check if the 'authtoken' header matches the PLUGIN_TOKEN environment variable
*/
export const authPluginToken = async ({ req }: { req: ApiRequestProps }) => {
const authtoken = req.headers.authtoken as string | undefined;
if (!authtoken) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
if (!PLUGIN_TOKEN) {
return Promise.reject('PLUGIN_TOKEN is not configured');
}
if (authtoken !== PLUGIN_TOKEN) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return true;
};
import jwt from 'jsonwebtoken';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import z from 'zod';
import type { NextApiRequest } from 'next';
import { serviceEnv } from '../../../env';
const PLUGIN_ACCESS_TOKEN_SECRET = serviceEnv.PLUGIN_ACCESS_TOKEN_SECRET;
const PLUGIN_ACCESS_TOKEN_EXPIRES_IN = serviceEnv.PLUGIN_ACCESS_TOKEN_EXPIRES_IN;
export const PluginAccessTokenPayloadSchema = z.object({
tmbId: z.string(),
teamId: z.string(),
toolId: z.string()
});
export type PluginAccessTokenPayload = z.infer<typeof PluginAccessTokenPayloadSchema>;
/**
* Generate plugin access token
* JWT with tmbId and toolId in payload
*/
export const generatePluginAccessToken = (payload: PluginAccessTokenPayload): string => {
const data = PluginAccessTokenPayloadSchema.parse(payload);
const token = jwt.sign(data, PLUGIN_ACCESS_TOKEN_SECRET, {
expiresIn: PLUGIN_ACCESS_TOKEN_EXPIRES_IN
});
return token;
};
/**
* Verify and decode plugin access token
* Returns the payload if valid, otherwise rejects with error
*/
export const authPluginAccessToken = ({
req
}: {
req: NextApiRequest;
}): Promise<PluginAccessTokenPayload> => {
const token = req.headers.authorization?.split(' ')[1];
return new Promise((resolve, reject) => {
if (!token) {
return reject(ERROR_ENUM.unAuthorization);
}
jwt.verify(token, PLUGIN_ACCESS_TOKEN_SECRET, (err, decoded: any) => {
if (err) {
return reject(ERROR_ENUM.unAuthorization);
}
try {
const payload = PluginAccessTokenPayloadSchema.parse(decoded);
return resolve(payload);
} catch (error) {
return reject(ERROR_ENUM.unAuthorization);
}
});
});
};
import { connectionMongo, getMongoModel } from '../../common/mongo'; import { connectionMongo, getMongoModel } from '../../common/mongo';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
import { hashStr } from '@fastgpt/global/common/string/tools'; import { hashStr } from '@fastgpt/global/common/string/tools';
import { UserTagsEnum, type UserModelSchema } from '@fastgpt/global/support/user/type'; import { UserTagsSchema, type UserModelSchema } from '@fastgpt/global/support/user/type';
import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant'; import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant';
import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant';
import { LangEnum } from '@fastgpt/global/common/i18n/type'; import { LangEnum } from '@fastgpt/global/common/i18n/type';
...@@ -69,7 +69,7 @@ const UserSchema = new Schema({ ...@@ -69,7 +69,7 @@ const UserSchema = new Schema({
tags: { tags: {
type: [String], type: [String],
enum: UserTagsEnum.enum enum: UserTagsSchema.enum
}, },
meta: Object, meta: Object,
/** @deprecated */ /** @deprecated */
......
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { SystemCacheKeyEnum } from '@fastgpt/service/common/cache/type'; import { SystemCacheKeyEnum } from '@fastgpt/service/common/cache/type';
vi.mock('@fastgpt/service/core/app/tool/controller', () => ({
refreshSystemTools: vi.fn().mockResolvedValue([])
}));
import { initCache } from '@fastgpt/service/common/cache/init'; import { initCache } from '@fastgpt/service/common/cache/init';
import { refreshSystemTools } from '@fastgpt/service/core/app/tool/controller';
describe('initCache', () => { describe('initCache', () => {
beforeEach(() => { beforeEach(() => {
...@@ -18,15 +13,6 @@ describe('initCache', () => { ...@@ -18,15 +13,6 @@ describe('initCache', () => {
expect(global.systemCache).toBeDefined(); expect(global.systemCache).toBeDefined();
}); });
it('should set up systemTool cache entry', () => {
initCache();
const entry = global.systemCache[SystemCacheKeyEnum.systemTool];
expect(entry.versionKey).toBe('');
expect(entry.data).toEqual([]);
expect(entry.refreshFunc).toBe(refreshSystemTools);
expect(entry.devRefresh).toBe(true);
});
it('should set up modelPermission cache entry', () => { it('should set up modelPermission cache entry', () => {
initCache(); initCache();
const entry = global.systemCache[SystemCacheKeyEnum.modelPermission]; const entry = global.systemCache[SystemCacheKeyEnum.modelPermission];
...@@ -43,8 +29,8 @@ describe('initCache', () => { ...@@ -43,8 +29,8 @@ describe('initCache', () => {
it('should overwrite existing systemCache when called again', () => { it('should overwrite existing systemCache when called again', () => {
initCache(); initCache();
global.systemCache[SystemCacheKeyEnum.systemTool].versionKey = 'old'; global.systemCache[SystemCacheKeyEnum.modelPermission].versionKey = 'old';
initCache(); initCache();
expect(global.systemCache[SystemCacheKeyEnum.systemTool].versionKey).toBe(''); expect(global.systemCache[SystemCacheKeyEnum.modelPermission].versionKey).toBe('');
}); });
}); });
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
const mocks = vi.hoisted(() => ({
getSystemToolDetail: vi.fn(),
getInstance: vi.fn()
}));
vi.mock('@fastgpt/service/core/app/tool/systemTool/systemTool.repo', () => ({
SystemToolRepo: {
getInstance: mocks.getInstance
}
}));
import { getToolPreviewNode } from '@fastgpt/service/core/app/tool/presenter';
describe('getToolPreviewNode', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.getInstance.mockReturnValue({
getSystemToolDetail: mocks.getSystemToolDetail
});
});
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({
id: 'systemTool-weather',
version: '1.0.0',
status: 1,
source: 'system',
isToolSet: false,
avatar: 'weather.svg',
name: 'Weather',
intro: 'Weather query',
author: 'FastGPT',
tags: [],
toolDescription: 'Weather query',
currentCost: 0,
systemKeyCost: 1,
hasTokenFee: false,
hasSystemSecret: true,
secrets,
inputs: [
{
key: 'city',
label: 'City',
valueType: 'string',
renderTypeList: [FlowNodeInputTypeEnum.input],
required: true
}
],
outputs: []
});
const result = await getToolPreviewNode({
pluginId: 'systemTool-weather',
versionId: '1.0.0',
lang: 'en'
});
expect(result.inputs[0]).toEqual({
key: NodeInputKeyEnum.systemInputConfig,
label: '',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
inputList: secrets
});
expect(result.inputs[1]?.key).toBe('city');
});
it('keeps inputs unchanged when system tool has no secrets', async () => {
mocks.getSystemToolDetail.mockResolvedValueOnce({
id: 'systemTool-weather',
version: '1.0.0',
status: 1,
source: 'system',
isToolSet: false,
avatar: 'weather.svg',
name: 'Weather',
intro: 'Weather query',
author: 'FastGPT',
tags: [],
toolDescription: 'Weather query',
currentCost: 0,
systemKeyCost: 0,
hasTokenFee: false,
hasSystemSecret: false,
inputs: [
{
key: 'city',
label: 'City',
valueType: 'string',
renderTypeList: [FlowNodeInputTypeEnum.input],
required: true
}
],
outputs: []
});
const result = await getToolPreviewNode({
pluginId: 'systemTool-weather',
lang: 'en'
});
expect(result.inputs).toHaveLength(1);
expect(result.inputs[0]?.key).toBe('city');
});
it('returns plugin module preview for commercial workflow tools', async () => {
mocks.getSystemToolDetail.mockResolvedValueOnce({
id: 'commercial-workflow-tool',
version: 'workflow-version',
status: 1,
source: 'system',
isToolSet: false,
avatar: 'workflow.svg',
name: 'Workflow Tool',
intro: 'Workflow tool intro',
author: 'FastGPT',
tags: [],
toolDescription: 'Run workflow tool',
currentCost: 1,
systemKeyCost: 0,
hasTokenFee: true,
hasSystemSecret: false,
associatedPluginId: 'app-id',
inputs: [
{
key: 'query',
label: 'Query',
valueType: 'string',
renderTypeList: [FlowNodeInputTypeEnum.input],
required: true
}
],
outputs: []
});
const result = await getToolPreviewNode({
pluginId: 'commercial-workflow-tool',
versionId: 'workflow-version',
lang: 'en'
});
expect(result.flowNodeType).toBe(FlowNodeTypeEnum.pluginModule);
expect(result.pluginId).toBe('commercial-workflow-tool');
expect(result.toolConfig).toBeUndefined();
expect(result.isFolder).toBe(false);
expect(result.inputs[0]?.key).toBe('query');
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SystemToolSystemSecretStatusEnum } from '@fastgpt/global/core/app/tool/systemTool/constants';
const mocks = vi.hoisted(() => ({
listTools: vi.fn(),
getTool: vi.fn(),
findSystemTools: vi.fn(),
findSystemTool: vi.fn(),
findAppById: vi.fn(),
getAppLatestVersion: vi.fn(),
getAppVersionById: vi.fn(),
checkIsLatestVersion: vi.fn()
}));
vi.mock('@fastgpt/service/thirdProvider/fastgptPlugin', () => ({
pluginClient: {
listTools: mocks.listTools,
getTool: mocks.getTool
}
}));
vi.mock('@fastgpt/service/core/plugin/tool/systemToolSchema', () => ({
MongoSystemTool: {
find: mocks.findSystemTools,
findOne: mocks.findSystemTool
}
}));
vi.mock('@fastgpt/service/core/app/schema', () => ({
AppCollectionName: 'apps',
chatConfigType: {},
MongoApp: {
findById: mocks.findAppById
}
}));
vi.mock('@fastgpt/service/core/app/version/controller', () => ({
getAppLatestVersion: mocks.getAppLatestVersion,
getAppVersionById: mocks.getAppVersionById,
checkIsLatestVersion: mocks.checkIsLatestVersion
}));
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
const createPluginTool = ({
pluginId,
name,
tags = [],
hasSecret = false
}: {
pluginId: string;
name: string;
tags?: string[];
hasSecret?: boolean;
}) => ({
source: 'system',
isToolset: false,
hasSecret,
type: 'tool',
name: { en: name },
description: { en: `${name} intro` },
pluginId,
version: '1.0.0',
etag: `${pluginId}-etag`,
icon: `${pluginId}.svg`,
tags,
toolDescription: `${name} description`
});
const createToolConfig = ({
pluginId,
pluginOrder,
tags,
secretsVal
}: {
pluginId: string;
pluginOrder: number;
tags: string[];
secretsVal?: Record<string, unknown>;
}) => ({
pluginId: `systemTool-${pluginId}`,
pluginOrder,
secretsVal,
customConfig: {
name: pluginId,
version: '1.0.0',
tags
}
});
beforeEach(() => {
vi.clearAllMocks();
});
describe('SystemToolRepo.getSystemToolList', () => {
it('sorts by tag matched count before plugin order when tags are provided', async () => {
mocks.listTools.mockResolvedValue([
createPluginTool({ pluginId: 'low-order-single-match', name: 'Low order single match' }),
createPluginTool({ pluginId: 'high-order-two-match', name: 'High order two match' }),
createPluginTool({ pluginId: 'second-order-single-match', name: 'Second order single match' })
]);
mocks.findSystemTools.mockResolvedValue([
createToolConfig({
pluginId: 'low-order-single-match',
pluginOrder: 1,
tags: ['search']
}),
createToolConfig({
pluginId: 'high-order-two-match',
pluginOrder: 100,
tags: ['search', 'finance']
}),
createToolConfig({
pluginId: 'second-order-single-match',
pluginOrder: 2,
tags: ['search']
})
]);
const tools = await SystemToolRepo.getInstance().getSystemToolList({
tags: ['search', 'custom-tag', 'finance']
});
expect(mocks.listTools).toHaveBeenCalledWith({
op: undefined,
sources: undefined,
tags: ['search', 'finance']
});
expect(tools.map((tool) => tool.id)).toEqual([
'systemTool-high-order-two-match',
'systemTool-low-order-single-match',
'systemTool-second-order-single-match'
]);
});
it('keeps plugin order sorting when no tags are provided', async () => {
mocks.listTools.mockResolvedValue([
createPluginTool({ pluginId: 'third', name: 'Third' }),
createPluginTool({ pluginId: 'first', name: 'First' }),
createPluginTool({ pluginId: 'second', name: 'Second' })
]);
mocks.findSystemTools.mockResolvedValue([
createToolConfig({ pluginId: 'third', pluginOrder: 3, tags: [] }),
createToolConfig({ pluginId: 'first', pluginOrder: 1, tags: [] }),
createToolConfig({ pluginId: 'second', pluginOrder: 2, tags: [] })
]);
const tools = await SystemToolRepo.getInstance().getSystemToolList({});
expect(tools.map((tool) => tool.id)).toEqual([
'systemTool-first',
'systemTool-second',
'systemTool-third'
]);
});
it('sets system secret status from list hasSecret and saved secrets', async () => {
mocks.listTools.mockResolvedValue([
createPluginTool({ pluginId: 'no-secret', name: 'No secret' }),
createPluginTool({ pluginId: 'need-secret', name: 'Need secret', hasSecret: true }),
createPluginTool({ pluginId: 'configured-secret', name: 'Configured secret', hasSecret: true })
]);
mocks.findSystemTools.mockResolvedValue([
createToolConfig({ pluginId: 'no-secret', pluginOrder: 1, tags: [] }),
createToolConfig({ pluginId: 'need-secret', pluginOrder: 2, tags: [] }),
createToolConfig({
pluginId: 'configured-secret',
pluginOrder: 3,
tags: [],
secretsVal: { apiKey: 'configured' }
})
]);
const tools = await SystemToolRepo.getInstance().getSystemToolList({});
const statusMap = new Map(tools.map((tool) => [tool.id, tool.systemSecretStatus]));
expect(statusMap.get('systemTool-no-secret')).toBe(SystemToolSystemSecretStatusEnum.none);
expect(statusMap.get('systemTool-need-secret')).toBe(
SystemToolSystemSecretStatusEnum.unconfigured
);
expect(statusMap.get('systemTool-configured-secret')).toBe(
SystemToolSystemSecretStatusEnum.configured
);
expect(mocks.getTool).not.toHaveBeenCalled();
});
});
describe('SystemToolRepo.getSystemToolDetail', () => {
it('returns saved author for workflow tools', async () => {
mocks.findSystemTool.mockResolvedValue({
pluginId: 'systemTool-workflow-tool',
status: 'Normal',
currentCost: 0,
hasTokenFee: true,
systemKeyCost: 0,
pluginOrder: 0,
originCost: 0,
customConfig: {
name: 'Workflow Tool',
intro: 'Workflow intro',
version: 'workflow-version',
tags: [],
associatedPluginId: 'app-id',
author: 'Custom Author',
userGuide: 'Guide'
}
});
mocks.findAppById.mockReturnValue({
lean: () => Promise.resolve({ _id: 'app-id', avatar: 'app.svg' })
});
mocks.getAppLatestVersion.mockResolvedValue({
versionId: 'latest-version',
nodes: []
});
mocks.checkIsLatestVersion.mockResolvedValue(true);
const tool = await SystemToolRepo.getInstance().getSystemToolDetail({
pluginId: 'systemTool-workflow-tool'
});
expect(tool.author).toBe('Custom Author');
expect(tool.hasTokenFee).toBe(true);
});
});
...@@ -534,6 +534,7 @@ describe('formatUserQueryWithFiles', () => { ...@@ -534,6 +534,7 @@ describe('formatUserQueryWithFiles', () => {
file: { file: {
type: ChatFileTypeEnum.file, type: ChatFileTypeEnum.file,
name: 'bad.pdf', name: 'bad.pdf',
// 不以 / http ws 开头,会被 normalizeReadableFileUrl 过滤掉
url: 'chat/bad.pdf' url: 'chat/bad.pdf'
} }
} }
......
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PluginPermissionEnum } from '@fastgpt/global/sdk/fastgpt-plugin';
const mockGetToolFilePrefix = vi.hoisted(() => vi.fn());
const mockUploadChatFile = vi.hoisted(() => vi.fn());
const mockCreateUploadChatFileURL = vi.hoisted(() => vi.fn());
vi.mock('@fastgpt/service/common/s3/sources/chat', () => ({
getS3ChatSource: () => ({
getToolFilePrefix: mockGetToolFilePrefix,
uploadChatFile: mockUploadChatFile,
createUploadChatFileURL: mockCreateUploadChatFileURL
})
}));
import { InvokeProcessor } from '@fastgpt/service/support/invoke/invoke';
const createProcessor = () =>
new InvokeProcessor({
appId: 'app-1',
chatId: 'chat-1',
uId: 'user-1',
teamId: 'team-1',
tmbId: 'member-1',
permissions: [PluginPermissionEnum['file-upload:allow']]
});
describe('InvokeProcessor.handleFileUpload', () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetToolFilePrefix.mockReturnValue('chat/app-1/user-1/chat-1');
mockUploadChatFile.mockResolvedValue({
key: 'chat/app-1/user-1/chat-1/image.png',
accessUrl: {
bucket: 'fastgpt-private',
key: 'chat/app-1/user-1/chat-1/image.png',
url: 'https://example.com/api/system/file/download/token?filename=image.png'
}
});
});
it('上传文件内容并返回最终访问 URL', async () => {
const body = Buffer.from('image');
const result = await createProcessor().handleFileUpload({
filename: 'image.png',
body,
contentType: 'image/png'
});
expect(mockUploadChatFile).toHaveBeenCalledWith({
appId: 'app-1',
chatId: 'chat-1',
uId: 'user-1',
filename: 'image.png',
body,
contentType: 'image/png',
expiredTime: undefined
});
expect(mockCreateUploadChatFileURL).not.toHaveBeenCalled();
expect(result).toEqual({
url: 'https://example.com/api/system/file/download/token?filename=image.png'
});
});
it('缺少文件内容时不创建上传 URL', async () => {
await expect(
createProcessor().handleFileUpload({
filename: 'image.png',
contentType: 'image/png'
} as any)
).rejects.toThrow();
expect(mockUploadChatFile).not.toHaveBeenCalled();
expect(mockCreateUploadChatFileURL).not.toHaveBeenCalled();
});
});
import { FastGPTPluginClient } from '@fastgpt/global/sdk/fastgpt-plugin';
import { serviceEnv } from '../../env'; import { serviceEnv } from '../../env';
import {
FastGPTPluginClient,
type ToolAnswerType,
type ToolHandlerReturnType
} from '@fastgpt/global/sdk/fastgpt-plugin';
export const PLUGIN_BASE_URL = serviceEnv.PLUGIN_BASE_URL ?? ''; export const PLUGIN_BASE_URL = serviceEnv.PLUGIN_BASE_URL ?? '';
export const PLUGIN_TOKEN = serviceEnv.PLUGIN_TOKEN; export const PLUGIN_TOKEN = serviceEnv.PLUGIN_TOKEN;
...@@ -8,3 +12,124 @@ export const pluginClient = new FastGPTPluginClient({ ...@@ -8,3 +12,124 @@ export const pluginClient = new FastGPTPluginClient({
baseUrl: PLUGIN_BASE_URL, baseUrl: PLUGIN_BASE_URL,
token: PLUGIN_TOKEN token: PLUGIN_TOKEN
}); });
type RunPluginToolStreamParams = {
pluginId: string;
version?: string;
source?: string;
secrets?: Record<string, unknown>;
systemVar: Record<string, unknown>;
input: Record<string, unknown>;
childId?: string;
onMessage?: (message: ToolAnswerType) => void;
};
type ToolStreamMessage =
| {
type: 'response';
data: ToolHandlerReturnType;
}
| {
type: 'stream';
data: ToolAnswerType;
}
| {
type: 'error';
data: unknown;
};
const buildPluginApiUrl = (path: string) => {
const baseUrl = PLUGIN_BASE_URL.endsWith('/') ? PLUGIN_BASE_URL.slice(0, -1) : PLUGIN_BASE_URL;
return `${baseUrl}${path}`;
};
const parseSseData = (chunk: string): ToolStreamMessage | null => {
const data = chunk
.split('\n')
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice('data:'.length).trim())
.join('\n');
if (!data) return null;
try {
return JSON.parse(data);
} catch (error) {
return null;
}
};
export const runPluginToolStream = async ({
onMessage,
...params
}: RunPluginToolStreamParams): Promise<{
output?: ToolHandlerReturnType;
error?: unknown;
}> => {
const response = await fetch(buildPluginApiUrl('/api/tool/runStream'), {
method: 'POST',
headers: {
...(PLUGIN_TOKEN ? { Authorization: `Bearer ${PLUGIN_TOKEN}` } : {}),
Accept: 'text/event-stream',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
});
if (!response.ok) {
const error = await response.text().catch(() => response.statusText);
return {
error: error || response.statusText
};
}
if (!response.body) {
return {
error: 'Tool stream response body is empty'
};
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let output: ToolHandlerReturnType | undefined;
while (true) {
const { value, done } = await reader.read();
buffer += decoder.decode(value, { stream: !done });
const chunks = buffer.split('\n\n');
buffer = chunks.pop() || '';
for (const chunk of chunks) {
const message = parseSseData(chunk);
if (!message) continue;
if (message.type === 'stream') {
onMessage?.(message.data);
} else if (message.type === 'response') {
output = message.data;
} else if (message.type === 'error') {
return {
error: message.data
};
}
}
if (done) break;
}
const message = parseSseData(buffer);
if (message?.type === 'stream') {
onMessage?.(message.data);
} else if (message?.type === 'response') {
output = message.data;
} else if (message?.type === 'error') {
return {
error: message.data
};
}
return {
output
};
};
...@@ -7,9 +7,10 @@ import { ...@@ -7,9 +7,10 @@ import {
type NumberInputProps, type NumberInputProps,
type NumberInputFieldProps type NumberInputFieldProps
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React, { useRef } from 'react';
import MyIcon from '../../Icon'; import MyIcon from '../../Icon';
import { type UseFormRegister } from 'react-hook-form'; import { type UseFormRegister } from 'react-hook-form';
import { getNumberInputValue } from './utils';
type Props = Omit<NumberInputProps, 'onChange' | 'onBlur'> & { type Props = Omit<NumberInputProps, 'onChange' | 'onBlur'> & {
onChange?: (e?: number) => any; onChange?: (e?: number) => any;
...@@ -22,6 +23,7 @@ type Props = Omit<NumberInputProps, 'onChange' | 'onBlur'> & { ...@@ -22,6 +23,7 @@ type Props = Omit<NumberInputProps, 'onChange' | 'onBlur'> & {
}; };
const MyNumberInput = (props: Props) => { const MyNumberInput = (props: Props) => {
const isBlurFormattingRef = useRef(false);
const { const {
register, register,
name, name,
...@@ -37,7 +39,12 @@ const MyNumberInput = (props: Props) => { ...@@ -37,7 +39,12 @@ const MyNumberInput = (props: Props) => {
<NumberInput <NumberInput
{...restProps} {...restProps}
onBlur={(e) => { onBlur={(e) => {
const numE = e.target.value === '' ? '' : Number(e.target.value); isBlurFormattingRef.current = true;
setTimeout(() => {
isBlurFormattingRef.current = false;
});
const numE = getNumberInputValue(e.target.value, false);
if (onBlur) { if (onBlur) {
if (numE === '') { if (numE === '') {
// @ts-ignore // @ts-ignore
...@@ -65,7 +72,9 @@ const MyNumberInput = (props: Props) => { ...@@ -65,7 +72,9 @@ const MyNumberInput = (props: Props) => {
} }
}} }}
onChange={(e) => { onChange={(e) => {
const numE = e === '' ? '' : e.endsWith('.') || /^\d+\.0+$/.test(e) ? e : Number(e); const numE = getNumberInputValue(e, !isBlurFormattingRef.current);
isBlurFormattingRef.current = false;
if (onChange) { if (onChange) {
if (numE === '') { if (numE === '') {
// @ts-ignore // @ts-ignore
......
This diff is collapsed. Click to expand it.
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