V4.13.0 features (#5693)

* feat: concat usage code (#5657)

* feat: dataset parse queue (#5661)

* feat: chat usage concat (#5669)

* perf: search test usage

* feat: chat usage concat

* fix: ts

* fix: ts

* feat: chat node response store (#5675)

* feat: chat node response store

* limit export

* test

* add ai generate node (#5506)

* add node copilot

* apply code

* update dynamic input & output

* add code test

* usage

* dynamic input border render

* optimize input & output

* optimize code

* update style

* change card to popover

* prompt editor basic

* prompt editor

* handle key down

* update prompt

* merge

* fix

* fix

* fix

* perf: workflow performance (#5677)

* feat: chat node response store

* limit export

* perf: workflow performance

* remove log

* fix: app template get duplicate (#5682)

* fix: dynamic input lock & code param (#5680)

* fix: dynamic input lock & code param

* fix

* fix

* feat: multi node data sync & system tool hot-swapping (#5575)

* Enhance file upload functionality and system tool integration (#5257)

* Enhance file upload functionality and system tool integration

* Add supplementary documents and optimize the upload interface

* Refactor file plugin types and update upload configurations

* Refactor MinIO configuration variables and clean up API plugin handlers for improved readability and consistency

* File name change

* Refactor SystemTools component layout

* fix i18n

* fix

* fix

* fix

* optimize app logs sort (#5310)

* log keys config modal

* multiple select

* api

* fontsize

* code

* chatid

* fix build

* fix

* fix component

* change name

* log keys config

* fix

* delete unused

* fix

* chore: minio service class rewrite

* chore: s3 plugin upload

* feat: system global cache with multi node sync feature

* feat: cache

* chore: move images

* docs: update & remove useless code

* chore: resolve merge conflicts

* chore: adjust the code

* chore: adjust

* deps: upgrade @fastgpt-sdk/plugin to 0.1.17

* perf(s3): s3 config

* fix: cache syncKey refresh

* fix: update @fastgpt-sdk/plugin to v0.1.18 removing mongo definition for fixing vitest

* chore: adjust

---------

Co-authored-by: Ctrlz <143257420+ctrlz526@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Archer <545436317@qq.com>

* perf: s3 api code

* fix: toolbox empty when second open modal

* feat: http tool set (#5599)

* feat: http toolSet manual create front end

* feat: http toolSet manual create i18n

* feat: http toolSet manual create back end

* feat: auth, as tool param, adapt mcp

* fix: delete unused httpPlugin

* fix: delete FlowNodeTypeEnum.httpPlugin

* fix: AppTypeEnum include httpToolSet and httpPlugin

* fix

* delete console

* fix

* output schema

* fix

* fix bg

* fix base url

* fix

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* feat: app count

* perf: type check

* feat: catch error

* perf: plugin hot-swapping (#5688)

* perf: plugin hot-swapping

* chore: adjust code

* perf: cite data auth

* fix http toolset (#5689)

* temp

* fix http tool set

* fix

* template author hide

* dynamic IO ui

* fix: auth test

* fix dynamic input & output (#5690)

Co-authored-by: Archer <545436317@qq.com>

* fix: dynamic output id

* doc

* feat: model permission (#5666)

* feat(permission): model permission definition & api

* chore: support update model's collaborators

* feat: remove unauthedmodel when paste and import

* fix: type error

* fix: test setup global model list

* fix: http tool api

* chore: update fastgpt-sdk version

* chore: remove useless code

* chore: myModelList cache

* perf: user who is not manager can not configure model permission (FE)

* perf: model => Set

* feat: getMyModels moved to opensource code; cache the myModelList

* fix: type error

* fix dynamic input reference select type (#5694)

* remove unique index

* read file usage

* perf: connection error

* fix: abort token count

* fix: debug usage concat

* fix: immer clone object

* fix: immer clone object

* perf: throw error when error chat

* update audit i18n

* fix: 修复识别pptx文件后,返回内容顺序错乱问题 (#5696)

* fix: pptx sort error

* fix prompt editor (#5695)

* fix prompt editor

* fix

* fix: redis cache prefix (#5697)

* fix: redis cache prefix

* fix: cache

* fix: get model collaborator by model.model

* feat: hint for model per

* rename bucket name

* model ui

* doc

* doc

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: Ctrlz <143257420+ctrlz526@users.noreply.github.com>
Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>
Co-authored-by: Deepturn <33342819+Deepturn@users.noreply.github.com>
This commit is contained in:
Archer 2025-09-24 22:40:31 +08:00 committed by GitHub
parent 9f8f8dd3de
commit 051455238c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
249 changed files with 8797 additions and 4202 deletions

View File

@ -12,4 +12,4 @@
1. 选中的值存储在 hook 里,便于判断是否触发底部悬浮层
2. 悬浮层外层 Box 在 hook 里child 由调用组件实现
3. FastGPT/packages/web/hooks/useTableMultipleSelect.tsx 在这个文件下实现
3. FastGPT/packages/web/hooks/useTableMultipleSelect.tsx 在这个文件下实现

View File

@ -1,5 +1,14 @@
{
"title": "系统插件",
"description": "介绍如何使用和提交系统插件,以及各插件的填写说明",
"pages": ["dev_system_tool","how_to_submit_system_plugin","searxng_plugin_guide","google_search_plugin_guide","bing_search_plugin","doc2x_plugin_guide"]
}
"pages": [
"dev_system_tool",
"how_to_submit_system_plugin",
"upload_system_tool",
"searxng_plugin_guide",
"google_search_plugin_guide",
"bing_search_plugin",
"doc2x_plugin_guide",
"deepseek_plugin_guide"
]
}

View File

@ -0,0 +1,123 @@
---
title: 如何在线上传系统工具
description: FastGPT 系统工具在线上传指南
---
> 从 FastGPT 4.14.0 版本开始,系统管理员可以通过 Web 界面直接上传和更新系统工具,无需重新部署服务
## 权限要求
⚠️ **重要提示**:只有 **root 用户** 才能使用在线上传系统工具功能。
- 确保您已使用 `root` 账户登录 FastGPT
- 普通用户无法看到"导入/更新"按钮和删除功能
## 支持的文件格式
- **文件类型**`.js` 文件
- **文件大小**:最大 10MB
- **文件数量**:每次只能上传一个文件
## 上传步骤
### 1. 进入系统工具页面
1. 登录 FastGPT 管理后台
2. 导航到:**工作台** → **系统工具**
3. 确认页面右上角显示"导入/更新"按钮(只有 root 用户可见)
![](/imgs/plugins/entry.png)
### 2. 准备工具文件
在上传之前,请确保您的 `.js` 文件是从 fastgpt-plugin 项目中通过 `bun run build` 命令打包后的 dist/tools/built-in 文件夹下得到的
![](/imgs/plugins/file.png)
### 3. 执行上传
1. 点击 **"导入/更新"** 按钮
2. 在弹出的对话框中,点击文件选择区域
3. 选择您准备好的 `.js` 工具文件
4. 确认文件信息无误后,点击 **"确认导入"**
### 4. 上传过程
- 上传成功后会显示成功提示
- 页面自动刷新,新工具会出现在工具列表中
## 功能特点
### 工具管理
- **查看工具**:所有用户都可以查看已安装的系统工具
- **上传工具**:仅 root 用户可以上传新工具或更新现有工具
- **删除工具**:仅 root 用户可以删除已上传的工具
### 工具类型识别
系统会根据工具的配置自动识别工具类型:
- 🔧 **工具 (tools)**
- 🔍 **搜索 (search)**
- 🎨 **多模态 (multimodal)**
- 💬 **通讯 (communication)**
- 📦 **其他 (other)**
## 常见问题
### Q: 上传失败,提示"文件内容存在错误"
**可能原因:**
- fastgpt-plugin 项目不是最新的,导致打包的 `.js` 文件缺少正确的内容
- 工具配置格式不正确
**解决方案:**
1. 拉取最新的 fastgpt-plugin 项目重新进行 `bun run build` 获得打包后的 `.js` 文件
2. 检查本地插件运行是否成功
### Q: 无法看到"导入/更新"按钮
**原因:** 当前用户不是 root 用户
**解决方案:** 使用 root 账户重新登录
### Q: 文件上传超时
**可能原因:**
- 文件过大(超过 10MB
- 网络连接不稳定
**解决方案:**
1. 确认文件大小在限制范围内
2. 检查网络连接
3. 尝试重新上传
## 最佳实践
### 上传前检查
1. **代码测试**:在本地环境测试工具功能
2. **格式验证**:确保符合 FastGPT 工具规范
3. **文件大小**:保持文件在合理大小范围内
### 版本管理
- 建议为工具添加版本号注释
- 更新工具时,先备份原有版本
- 记录更新日志和功能变更
### 安全考虑
- 仅上传来源可信的工具文件
- 避免包含敏感信息或凭据
- 定期审查已安装的工具
### 存储方式
- 工具文件存储在 MinIO 中
- 工具元数据保存在 MongoDB 中
---
通过在线上传功能,您可以快速部署和管理系统工具,提高 FastGPT 的扩展性和灵活性。如遇到问题,请参考上述常见问题或联系技术支持。

View File

@ -90,6 +90,7 @@ description: FastGPT 文档目录
- [/docs/introduction/guide/plugins/doc2x_plugin_guide](/docs/introduction/guide/plugins/doc2x_plugin_guide)
- [/docs/introduction/guide/plugins/google_search_plugin_guide](/docs/introduction/guide/plugins/google_search_plugin_guide)
- [/docs/introduction/guide/plugins/searxng_plugin_guide](/docs/introduction/guide/plugins/searxng_plugin_guide)
- [/docs/introduction/guide/plugins/upload_system_tool](/docs/introduction/guide/plugins/upload_system_tool)
- [/docs/introduction/guide/team_permissions/invitation_link](/docs/introduction/guide/team_permissions/invitation_link)
- [/docs/introduction/guide/team_permissions/team_roles_permissions](/docs/introduction/guide/team_permissions/team_roles_permissions)
- [/docs/introduction/index](/docs/introduction/index)
@ -105,7 +106,7 @@ description: FastGPT 文档目录
- [/docs/upgrading/4-12/4122](/docs/upgrading/4-12/4122)
- [/docs/upgrading/4-12/4123](/docs/upgrading/4-12/4123)
- [/docs/upgrading/4-12/4124](/docs/upgrading/4-12/4124)
- [/docs/upgrading/4-12/4125](/docs/upgrading/4-12/4125)
- [/docs/upgrading/4-13/4130](/docs/upgrading/4-13/4130)
- [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40)
- [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41)
- [/docs/upgrading/4-8/42](/docs/upgrading/4-8/42)

View File

@ -1,26 +0,0 @@
---
title: 'V4.12.5(进行中)'
description: 'FastGPT V4.12.5 更新说明'
---
## 🚀 新增内容
## ⚙️ 优化
1. 系统工具增加对应 author 名字显示。同时使用安全的 I18n 翻译。
## 🐛 修复
1. debug 模式下,全局变量未传递。
2. debug 模式下,前方节点参数无法传递至后方节点。
3. 调试模式下,开启“自动执行”,会跳过外部变量的填写。
4. 自动语音回复未生效。
5. 节点复制,报错捕获配置丢失。
6. “猜你想问”的自定义提示词,保存时,上一次的值会被置空。
7. 配置了二级路由的情况下,知识库检索出来的图片地址拼接异常。
8. Prompt 编辑器,键盘输入时会清除掉 Markdown 标记。
## 🔨 插件更新
1. 新增火山引擎融合信息搜索工具。

View File

@ -1,5 +1,5 @@
{
"title": "4.12.x",
"description": "",
"pages": ["4125", "4124", "4123", "4122", "4121", "4120"]
"pages": ["4124", "4123", "4122", "4121", "4120"]
}

View File

@ -0,0 +1,83 @@
---
title: 'V4.13.0(进行中)'
description: 'FastGPT V4.13.0 更新说明'
---
## 更新指南
### 1. 更新镜像:
- 更新 FastGPT 镜像tag: v4.13.0
- 更新 FastGPT 商业版镜像tag: v4.13.0
- 更新 fastgpt-plugin 镜像 tag: v0.2.0
- mcp_server 无需更新
- Sandbox 无需更新
- AIProxy 无需更新
### 2. 更新环境变量
1. 更新 `fastgpt-plugin` 环境变量名字,并新增`S3_PLUGIN_BUCKET`、`MONGODB_URI`、`REDIS_URL`值。
```
S3_EXTERNAL_BASE_URL=https://xxx.com # S3 外网地址
S3_ENDPOINT=localhost
S3_PORT=9000
S3_USE_SSL=false
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_TOOL_BUCKET=fastgpt-tool # 系统工具,创建的临时文件,存储的桶,要求公开读私有写。
S3_PLUGIN_BUCKET=fastgpt-plugin # 系统插件热安装文件的桶,私有读写。
RETENTION_DAYS=15 # 系统工具临时文件保存天数
MONGODB_URI=mongodb://myusername:mypassword@mongo:27017/fastgpt?authSource=admin # MongoDB 链接参数
REDIS_URL=redis://default:mypassword@redis:6379 # Redis 链接参数
```
2. 增加`fastgpt`和`fastgpt-pro(商业版)` s3 相关环境变量。
```
# S3 外网地址
S3_EXTERNAL_BASE_URL=
S3_ENDPOINT=localhost
S3_PORT=9000
S3_USE_SSL=false
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_PLUGIN_BUCKET=fastgpt-plugin # 系统插件热安装文件的桶,私有读写。
```
## 🚀 新增内容
1. 应用新增 HTTP 工具集类型,取代原 HTTP 插件。
2. 支持系统管理员通过文件形式快速安装系统工具。
3. 团队管理员支持分配模型权限。
4. 代码运行节点支持 AI 辅助生成。
5. 知识库文件解析支持配置最大并发数。(开源版通过 config.json 文件中`systemEnv.datasetParseMaxProcess`属性配置,商业版通过 admin 后台配置。)
## ⚙️ 优化
1. 系统工具增加对应 author 名字显示。同时使用安全的 I18n 翻译。
2. 计量计费账单推送和合并逻辑。
3. 对话记录中,节点详情单独分表存储。
4. 删除 chat_items 中无效的 dataId 索引。
5. 工作流UI性能优化减少 UI 重绘。
6. 对话中,知识库引用鉴权采用整个对话框鉴权,而不是单条记录。
7. 工作流动态输入输出变量交互优化。
## 🐛 修复
1. debug 模式下,全局变量未传递。
2. debug 模式下,前方节点参数无法传递至后方节点。
3. 调试模式下,开启“自动执行”,会跳过外部变量的填写。
4. 自动语音回复未生效。
5. 节点复制,报错捕获配置丢失。
6. “猜你想问”的自定义提示词,保存时,上一次的值会被置空。
7. 配置了二级路由的情况下,知识库检索出来的图片地址拼接异常。
8. Prompt 编辑器,键盘输入时会清除掉 Markdown 标记。
9. 知识库集合页面,有训练数据时候无法自动刷新页面。
10. 工作流快速添加节点弹窗,工具箱页面二次打开时为空。
11. PPTX 文件解析顺序错误。
## 🔨 插件更新
1. 新增火山引擎融合信息搜索工具。

View File

@ -0,0 +1,5 @@
{
"title": "4.13.x",
"description": "",
"pages": ["4130"]
}

View File

@ -4,6 +4,8 @@
"description": "FastGPT 版本更新介绍及升级操作",
"pages": [
"index",
"---4.13.x---",
"...4-13",
"---4.12.x---",
"...4-12",
"---4.11.x---",

View File

@ -8,7 +8,7 @@
"document/content/docs/faq/other.mdx": "2025-08-04T22:07:52+08:00",
"document/content/docs/faq/points_consumption.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/introduction/cloud.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/introduction/commercial.mdx": "2025-08-04T22:07:52+08:00",
"document/content/docs/introduction/commercial.mdx": "2025-09-21T23:09:46+08:00",
"document/content/docs/introduction/development/community.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/introduction/development/configuration.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/development/custom-models/bge-rerank.mdx": "2025-07-23T21:35:03+08:00",
@ -87,6 +87,7 @@
"document/content/docs/introduction/guide/plugins/doc2x_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/plugins/google_search_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/plugins/searxng_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-09-20T19:49:21+08:00",
"document/content/docs/introduction/guide/team_permissions/invitation_link.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/team_permissions/team_roles_permissions.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/index.en.mdx": "2025-07-23T21:35:03+08:00",
@ -99,7 +100,7 @@
"document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
"document/content/docs/toc.mdx": "2025-09-17T22:29:56+08:00",
"document/content/docs/toc.mdx": "2025-09-23T14:19:37+08:00",
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00",
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
@ -109,7 +110,7 @@
"document/content/docs/upgrading/4-12/4122.mdx": "2025-09-07T14:41:48+08:00",
"document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T20:55:14+08:00",
"document/content/docs/upgrading/4-12/4124.mdx": "2025-09-17T22:29:56+08:00",
"document/content/docs/upgrading/4-12/4125.mdx": "2025-09-18T16:15:12+08:00",
"document/content/docs/upgrading/4-13/4130.mdx": "2025-09-24T21:54:28+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -130,10 +130,12 @@ export type FastGPTFeConfigsType = {
export type SystemEnvType = {
openapiPrefix?: string;
tokenWorkers: number; // token count max worker
datasetParseMaxProcess: number;
vectorMaxProcess: number;
qaMaxProcess: number;
vlmMaxProcess: number;
tokenWorkers: number; // token count max worker
hnswEfSearch: number;
hnswMaxScanTuples: number;

View File

@ -11,10 +11,13 @@ export enum AppTypeEnum {
simple = 'simple',
workflow = 'advanced',
plugin = 'plugin',
httpPlugin = 'httpPlugin',
toolSet = 'toolSet',
toolSet = 'toolSet', // 'mcp'
httpToolSet = 'httpToolSet',
tool = 'tool',
hidden = 'hidden'
hidden = 'hidden',
// deprecated
httpPlugin = 'httpPlugin'
}
export const AppFolderTypeList = [AppTypeEnum.folder, AppTypeEnum.httpPlugin];

View File

@ -1,390 +0,0 @@
import { getNanoid } from '../../../common/string/tools';
import { type OpenApiJsonSchema } from './type';
import yaml from 'js-yaml';
import type { OpenAPIV3 } from 'openapi-types';
import { type FlowNodeInputItemType, type FlowNodeOutputItemType } from '../../workflow/type/io';
import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../../workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '../../workflow/constants';
import { PluginInputModule } from '../../workflow/template/system/pluginInput';
import { PluginOutputModule } from '../../workflow/template/system/pluginOutput';
import { HttpNode468 } from '../../workflow/template/system/http468';
import { type HttpParamAndHeaderItemType } from '../../workflow/type/io';
import { type StoreNodeItemType } from '../../workflow/type/node';
import { HttpImgUrl } from '../../../common/file/image/constants';
import SwaggerParser from '@apidevtools/swagger-parser';
import { getHandleId } from '../../workflow/utils';
import { type CreateHttpPluginChildrenPros } from '../controller';
import { AppTypeEnum } from '../constants';
import type { StoreEdgeItemType } from '../../workflow/type/edge';
export const str2OpenApiSchema = async (yamlStr = ''): Promise<OpenApiJsonSchema> => {
try {
const data = (() => {
try {
return JSON.parse(yamlStr);
} catch (jsonError) {
return yaml.load(yamlStr, { schema: yaml.FAILSAFE_SCHEMA });
}
})();
const jsonSchema = (await SwaggerParser.parse(data)) as OpenAPIV3.Document;
const serverPath = jsonSchema.servers?.[0].url || '';
const pathData = Object.keys(jsonSchema.paths)
.map((path) => {
const methodData: any = data.paths[path];
return Object.keys(methodData)
.filter((method) =>
['get', 'post', 'put', 'delete', 'patch'].includes(method.toLocaleLowerCase())
)
.map((method) => {
const methodInfo = methodData[method];
if (methodInfo.deprecated) return;
const result = {
path,
method,
name: methodInfo.operationId || path,
description: methodInfo.description || methodInfo.summary,
params: methodInfo.parameters,
request: methodInfo?.requestBody,
response: methodInfo.responses
};
return result;
});
})
.flat()
.filter(Boolean) as OpenApiJsonSchema['pathData'];
return { pathData, serverPath };
} catch (err) {
throw new Error('Invalid Schema');
}
};
export const getType = (schema: { type: string; items?: { type: string } }) => {
const typeMap: { [key: string]: WorkflowIOValueTypeEnum } = {
string: WorkflowIOValueTypeEnum.arrayString,
number: WorkflowIOValueTypeEnum.arrayNumber,
integer: WorkflowIOValueTypeEnum.arrayNumber,
boolean: WorkflowIOValueTypeEnum.arrayBoolean,
object: WorkflowIOValueTypeEnum.arrayObject
};
if (schema?.type === 'integer') {
return WorkflowIOValueTypeEnum.number;
}
if (schema?.type === 'array' && schema?.items) {
const itemType = typeMap[schema.items.type];
if (itemType) {
return itemType;
}
}
return schema?.type as WorkflowIOValueTypeEnum;
};
export const httpApiSchema2Plugins = async ({
parentId,
apiSchemaStr = '',
customHeader = ''
}: {
parentId: string;
apiSchemaStr?: string;
customHeader?: string;
}): Promise<CreateHttpPluginChildrenPros[]> => {
const jsonSchema = await str2OpenApiSchema(apiSchemaStr);
const baseUrl = jsonSchema.serverPath;
return jsonSchema.pathData.map((item) => {
const pluginOutputId = getNanoid();
const httpId = getNanoid();
const pluginInputId = getNanoid();
const inputIdMap = new Map();
const pluginOutputKey = 'result';
const properties = item.request?.content?.['application/json']?.schema?.properties;
const propsKeys = properties ? Object.keys(properties) : [];
const pluginInputs: FlowNodeInputItemType[] = [
...(item.params?.map((param: any) => {
return {
key: param.name,
valueType: getType(param.schema),
label: param.name,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: param.required,
description: param.description,
toolDescription: param.description,
canEdit: true
};
}) || []),
...(propsKeys?.map((key) => {
const prop = properties[key];
return {
key,
valueType: getType(prop),
label: key,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: false,
description: prop.description,
toolDescription: prop.description,
canEdit: true
};
}) || [])
];
const pluginOutputs: FlowNodeOutputItemType[] = [
...(item.params?.map((param: any) => {
const id = getNanoid();
inputIdMap.set(param.name, id);
return {
id,
key: param.name,
valueType: getType(param.schema),
label: param.name,
type: FlowNodeOutputTypeEnum.source
};
}) || []),
...(propsKeys?.map((key) => {
const id = getNanoid();
inputIdMap.set(key, id);
return {
id,
key,
valueType: getType(properties[key]),
label: key,
type: FlowNodeOutputTypeEnum.source,
edit: true
};
}) || [])
];
const httpInputs: FlowNodeInputItemType[] = [
...(item.params?.map((param: any) => {
return {
key: param.name,
valueType: getType(param.schema),
label: param.name,
renderTypeList: [FlowNodeInputTypeEnum.reference],
canEdit: true,
value: [pluginInputId, inputIdMap.get(param.name)]
};
}) || []),
...(propsKeys?.map((key) => {
return {
key,
valueType: getType(properties[key]),
label: key,
renderTypeList: [FlowNodeInputTypeEnum.reference],
canEdit: true,
value: [pluginInputId, inputIdMap.get(key)]
};
}) || [])
];
/* http node setting */
const httpNodeParams: HttpParamAndHeaderItemType[] = [];
const httpNodeHeaders: HttpParamAndHeaderItemType[] = [];
let httpNodeBody = '{}';
const requestUrl = `${baseUrl}${item.path}`;
if (item.params && item.params.length > 0) {
for (const param of item.params) {
if (param.in === 'header') {
httpNodeHeaders.push({
key: param.name,
type: getType(param.schema) || WorkflowIOValueTypeEnum.string,
value: `{{${param.name}}}`
});
} else if (param.in === 'body') {
httpNodeBody = JSON.stringify(
{ ...JSON.parse(httpNodeBody), [param.name]: `{{${param.name}}}` },
null,
2
);
} else if (param.in === 'query') {
httpNodeParams.push({
key: param.name,
type: getType(param.schema) || WorkflowIOValueTypeEnum.string,
value: `{{${param.name}}}`
});
}
}
}
if (item.request) {
const properties = item.request?.content?.['application/json']?.schema?.properties || {};
const keys = Object.keys(properties);
if (keys.length > 0) {
httpNodeBody = JSON.stringify(
keys.reduce((acc: any, key) => {
acc[key] = `{{${key}}}`;
return acc;
}, {}),
null,
2
);
}
}
if (customHeader) {
const headersObj = (() => {
try {
return JSON.parse(customHeader) as Record<string, string>;
} catch (err) {
return {};
}
})();
for (const key in headersObj) {
httpNodeHeaders.push({
key,
type: WorkflowIOValueTypeEnum.string,
// @ts-ignore
value: headersObj[key]
});
}
}
/* Combine complete modules */
const nodes: StoreNodeItemType[] = [
{
nodeId: pluginInputId,
name: PluginInputModule.name,
intro: PluginInputModule.intro,
avatar: PluginInputModule.avatar,
flowNodeType: PluginInputModule.flowNodeType,
showStatus: PluginInputModule.showStatus,
position: {
x: 473.55206291900333,
y: -145.65080850146154
},
version: PluginInputModule.version,
inputs: pluginInputs,
outputs: pluginOutputs
},
{
nodeId: pluginOutputId,
name: PluginOutputModule.name,
intro: PluginOutputModule.intro,
avatar: PluginOutputModule.avatar,
flowNodeType: PluginOutputModule.flowNodeType,
showStatus: PluginOutputModule.showStatus,
position: {
x: 1847.5956872650024,
y: 5.114324648101558
},
version: PluginOutputModule.version,
inputs: [
{
key: pluginOutputKey,
valueType: WorkflowIOValueTypeEnum.string,
label: pluginOutputKey,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: false,
description: '',
canEdit: true,
value: [httpId, 'httpRawResponse']
}
],
outputs: [
{
id: pluginOutputId,
key: pluginOutputKey,
valueType: WorkflowIOValueTypeEnum.string,
label: pluginOutputKey,
type: FlowNodeOutputTypeEnum.static
}
]
},
{
nodeId: httpId,
name: HttpNode468.name,
intro: HttpNode468.intro,
avatar: HttpNode468.avatar,
flowNodeType: HttpNode468.flowNodeType,
showStatus: true,
position: {
x: 1188.947986995841,
y: -473.52694296182904
},
version: HttpNode468.version,
inputs: [
HttpNode468.inputs[0],
...httpInputs,
{
key: 'system_httpMethod',
renderTypeList: [FlowNodeInputTypeEnum.custom],
valueType: WorkflowIOValueTypeEnum.string,
label: '',
value: item.method.toUpperCase(),
required: true
},
{
key: 'system_httpReqUrl',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.string,
label: '',
description: 'core.module.input.description.Http Request Url',
placeholder: 'https://api.ai.com/getInventory',
required: false,
value: requestUrl
},
{
key: 'system_httpHeader',
renderTypeList: [FlowNodeInputTypeEnum.custom],
valueType: WorkflowIOValueTypeEnum.any,
value: httpNodeHeaders,
label: '',
description: 'core.module.input.description.Http Request Header',
placeholder: 'core.module.input.description.Http Request Header',
required: false
},
{
key: 'system_httpParams',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
value: httpNodeParams,
label: '',
required: false
},
{
key: 'system_httpJsonBody',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
value: httpNodeBody,
label: '',
required: false
}
],
outputs: HttpNode468.outputs
}
];
const edges: StoreEdgeItemType[] = [
{
source: pluginInputId,
target: httpId,
sourceHandle: getHandleId(pluginInputId, 'source', 'right'),
targetHandle: getHandleId(httpId, 'target', 'left')
},
{
source: httpId,
target: pluginOutputId,
sourceHandle: getHandleId(httpId, 'source', 'right'),
targetHandle: getHandleId(pluginOutputId, 'target', 'left')
}
];
return {
name: item.name,
avatar: HttpImgUrl,
intro: item.description,
parentId,
type: AppTypeEnum.plugin,
modules: nodes,
edges,
pluginData: {
pluginUniId: item.name
}
};
});
};

View File

@ -1,4 +1,4 @@
export type PathDataType = {
type PathDataType = {
name: string;
description: string;
method: string;

View File

@ -0,0 +1,174 @@
import { getNanoid } from '../../../common/string/tools';
import type { PathDataType } from './type';
import { type RuntimeNodeItemType } from '../../workflow/runtime/type';
import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../workflow/node/constant';
import { type HttpToolConfigType } from '../type';
import { PluginSourceEnum } from '../plugin/constants';
import { jsonSchema2NodeInput, jsonSchema2NodeOutput } from '../jsonschema';
import { type StoreSecretValueType } from '../../../common/secret/type';
import { type JsonSchemaPropertiesItemType } from '../jsonschema';
import { NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../workflow/constants';
import { i18nT } from '../../../../web/i18n/utils';
export const getHTTPToolSetRuntimeNode = ({
name,
avatar,
baseUrl = '',
customHeaders = '',
apiSchemaStr = '',
toolList = [],
headerSecret
}: {
name?: string;
avatar?: string;
baseUrl?: string;
customHeaders?: string;
apiSchemaStr?: string;
toolList?: HttpToolConfigType[];
headerSecret?: StoreSecretValueType;
}): RuntimeNodeItemType => {
return {
nodeId: getNanoid(16),
flowNodeType: FlowNodeTypeEnum.toolSet,
avatar,
intro: 'HTTP Tools',
toolConfig: {
httpToolSet: {
baseUrl,
toolList,
headerSecret,
customHeaders,
apiSchemaStr,
toolId: ''
}
},
inputs: [],
outputs: [],
name: name || '',
version: ''
};
};
export const getHTTPToolRuntimeNode = ({
tool,
nodeId,
avatar = 'core/app/type/httpToolsFill',
parentId
}: {
tool: Omit<HttpToolConfigType, 'path' | 'method'>;
nodeId?: string;
avatar?: string;
parentId: string;
}): RuntimeNodeItemType => {
return {
nodeId: nodeId || getNanoid(16),
flowNodeType: FlowNodeTypeEnum.tool,
avatar,
intro: tool.description,
toolConfig: {
httpTool: {
toolId: `${PluginSourceEnum.http}-${parentId}/${tool.name}`
}
},
inputs: jsonSchema2NodeInput(tool.inputSchema),
outputs: [
...jsonSchema2NodeOutput(tool.outputSchema),
{
id: NodeOutputKeyEnum.rawResponse,
key: NodeOutputKeyEnum.rawResponse,
required: true,
label: i18nT('workflow:raw_response'),
description: i18nT('workflow:tool_raw_response_description'),
valueType: WorkflowIOValueTypeEnum.any,
type: FlowNodeOutputTypeEnum.static
}
],
name: tool.name,
version: ''
};
};
export const pathData2ToolList = async (
pathData: PathDataType[]
): Promise<HttpToolConfigType[]> => {
try {
return pathData.map((pathItem) => {
const inputProperties: Record<string, JsonSchemaPropertiesItemType> = {};
const inputRequired: string[] = [];
const outputProperties: Record<string, JsonSchemaPropertiesItemType> = {};
const outputRequired: string[] = [];
if (pathItem.params && Array.isArray(pathItem.params)) {
pathItem.params.forEach((param) => {
if (param.name && param.schema) {
inputProperties[param.name] = {
type: param.schema.type || 'any',
description: param.description || ''
};
if (param.required) {
inputRequired.push(param.name);
}
}
});
}
if (pathItem.request?.content?.['application/json']?.schema) {
const requestSchema = pathItem.request.content['application/json'].schema;
if (requestSchema.properties) {
Object.entries(requestSchema.properties).forEach(([key, value]: [string, any]) => {
inputProperties[key] = {
type: value.type || 'any',
description: value.description || ''
};
});
}
if (requestSchema.required && Array.isArray(requestSchema.required)) {
inputRequired.push(...requestSchema.required);
}
}
const responseToProcess =
pathItem.response?.['200'] ||
pathItem.response?.['201'] ||
pathItem.response?.['202'] ||
pathItem.response?.default;
if (responseToProcess?.content?.['application/json']?.schema) {
const responseSchema = responseToProcess.content['application/json'].schema;
if (responseSchema.properties) {
Object.entries(responseSchema.properties).forEach(([key, value]: [string, any]) => {
outputProperties[key] = {
type: value.type || 'any',
description: value.description || ''
};
});
}
if (responseSchema.required && Array.isArray(responseSchema.required)) {
outputRequired.push(...responseSchema.required);
}
}
return {
name: pathItem.name,
description: pathItem.description || pathItem.name,
path: pathItem.path,
method: pathItem.method?.toLowerCase(),
inputSchema: {
type: 'object',
properties: inputProperties,
required: inputRequired
},
outputSchema: {
type: 'object',
properties: outputProperties,
required: outputRequired
}
};
});
} catch (error) {
console.error('Error converting API schema to tool list:', error);
return [];
}
};

View File

@ -1,10 +1,15 @@
import { WorkflowIOValueTypeEnum } from '../workflow/constants';
import { FlowNodeInputTypeEnum } from '../workflow/node/constant';
import type { FlowNodeInputItemType } from '../workflow/type/io';
import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../workflow/node/constant';
import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '../workflow/type/io';
import SwaggerParser from '@apidevtools/swagger-parser';
import yaml from 'js-yaml';
import type { OpenAPIV3 } from 'openapi-types';
import type { OpenApiJsonSchema } from './httpTools/type';
type SchemaInputValueType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
export type JsonSchemaPropertiesItemType = {
description?: string;
'x-tool-description'?: string;
type: SchemaInputValueType;
enum?: string[];
minimum?: number;
@ -16,6 +21,11 @@ export type JSONSchemaInputType = {
properties?: Record<string, JsonSchemaPropertiesItemType>;
required?: string[];
};
export type JSONSchemaOutputType = {
type: SchemaInputValueType;
properties?: Record<string, JsonSchemaPropertiesItemType>;
required?: string[];
};
export const getNodeInputTypeFromSchemaInputType = ({
type,
@ -79,8 +89,119 @@ export const jsonSchema2NodeInput = (jsonSchema: JSONSchemaInputType): FlowNodeI
label: key,
valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }),
description: value.description,
toolDescription: value.description || key,
toolDescription: value['x-tool-description'] ?? value.description ?? key,
required: jsonSchema?.required?.includes(key),
...getNodeInputRenderTypeFromSchemaInputType(value)
}));
};
export const jsonSchema2NodeOutput = (
jsonSchema: JSONSchemaOutputType
): FlowNodeOutputItemType[] => {
return Object.entries(jsonSchema?.properties || {}).map(([key, value]) => ({
id: key,
key,
label: key,
required: jsonSchema?.required?.includes(key),
type: FlowNodeOutputTypeEnum.static,
valueType: getNodeInputTypeFromSchemaInputType({ type: value.type, arrayItems: value.items }),
description: value.description,
toolDescription: value['x-tool-description'] ?? value.description ?? key
}));
};
export const str2OpenApiSchema = async (yamlStr = ''): Promise<OpenApiJsonSchema> => {
try {
const data = (() => {
try {
return JSON.parse(yamlStr);
} catch (jsonError) {
return yaml.load(yamlStr, { schema: yaml.FAILSAFE_SCHEMA });
}
})();
const jsonSchema = (await SwaggerParser.dereference(data)) as OpenAPIV3.Document;
const serverPath = (() => {
if (jsonSchema.servers && jsonSchema.servers.length > 0) {
return jsonSchema.servers[0].url || '';
}
if (data.host || data.basePath) {
const scheme = data.schemes && data.schemes.length > 0 ? data.schemes[0] : 'https';
const host = data.host || '';
const basePath = data.basePath || '';
return `${scheme}://${host}${basePath}`;
}
return '';
})();
const pathData = Object.keys(jsonSchema.paths)
.map((path) => {
const methodData: any = jsonSchema.paths[path];
return Object.keys(methodData)
.filter((method) =>
['get', 'post', 'put', 'delete', 'patch'].includes(method.toLocaleLowerCase())
)
.map((method) => {
const methodInfo = methodData[method];
if (methodInfo.deprecated) return;
const requestBody = (() => {
if (methodInfo?.requestBody) {
return methodInfo.requestBody;
}
if (methodInfo.parameters) {
const bodyParam = methodInfo.parameters.find(
(param: OpenAPIV3.ParameterObject) => param.in === 'body'
);
if (bodyParam) {
return {
content: {
'application/json': {
schema: bodyParam.schema
}
}
};
}
}
return undefined;
})();
const result = {
path,
method,
name: methodInfo.operationId || path,
description: methodInfo.description || methodInfo.summary,
params: methodInfo.parameters,
request: requestBody,
response: methodInfo.responses
};
return result;
});
})
.flat()
.filter(Boolean) as OpenApiJsonSchema['pathData'];
return { pathData, serverPath };
} catch (err) {
throw new Error('Invalid Schema');
}
};
export const getSchemaValueType = (schema: { type: string; items?: { type: string } }) => {
const typeMap: { [key: string]: WorkflowIOValueTypeEnum } = {
string: WorkflowIOValueTypeEnum.arrayString,
number: WorkflowIOValueTypeEnum.arrayNumber,
integer: WorkflowIOValueTypeEnum.arrayNumber,
boolean: WorkflowIOValueTypeEnum.arrayBoolean,
object: WorkflowIOValueTypeEnum.arrayObject
};
if (schema?.type === 'integer') {
return WorkflowIOValueTypeEnum.number;
}
if (schema?.type === 'array' && schema?.items) {
const itemType = typeMap[schema.items.type];
if (itemType) {
return itemType;
}
}
return schema?.type as WorkflowIOValueTypeEnum;
};

View File

@ -1,14 +1,6 @@
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../workflow/constants';
import { NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../workflow/constants';
import { i18nT } from '../../../../web/i18n/utils';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '../../workflow/node/constant';
import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../workflow/node/constant';
import { type McpToolConfigType } from '../type';
import { type RuntimeNodeItemType } from '../../workflow/runtime/type';
import { type StoreSecretValueType } from '../../../common/secret/type';

View File

@ -3,6 +3,7 @@ export enum PluginSourceEnum {
systemTool = 'systemTool', // FastGPT-plugin tools, pure code.
commercial = 'commercial', // configured in Pro, with associatedPluginId. Specially, commercial-dalle3 is a systemTool
mcp = 'mcp', // mcp
http = 'http', // http
// @deprecated
community = 'community' // this is deprecated, will be replaced by systemTool
}

View File

@ -56,6 +56,9 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & {
inputList?: FlowNodeInputItemType['inputList'];
inputListVal?: Record<string, any>;
hasSystemSecret?: boolean;
// Plugin source type
toolSource?: 'uploaded' | 'built-in';
};
export type SystemPluginTemplateListItemType = Omit<

View File

@ -60,5 +60,11 @@ export function splitCombinePluginId(id: string) {
pluginId
};
}
if (source === 'http') {
return {
source: PluginSourceEnum.http,
pluginId
};
}
return { source, pluginId: id };
}

View File

@ -15,7 +15,7 @@ import type { ParentIdType } from '../../common/parentFolder/type';
import { FlowNodeInputTypeEnum } from '../../core/workflow/node/constant';
import type { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type';
import type { SourceMemberType } from '../../support/user/type';
import type { JSONSchemaInputType } from './jsonschema';
import type { JSONSchemaInputType, JSONSchemaOutputType } from './jsonschema';
export type AppSchema = {
_id: string;
@ -120,6 +120,15 @@ export type McpToolConfigType = {
inputSchema: JSONSchemaInputType;
};
export type HttpToolConfigType = {
name: string;
description: string;
inputSchema: JSONSchemaInputType;
outputSchema: JSONSchemaOutputType;
path: string;
method: string;
};
/* app chat config type */
export type AppChatConfigType = {
welcomeText?: string;

View File

@ -20,6 +20,7 @@ import type { WorkflowInteractiveResponseType } from '../workflow/template/syste
import type { FlowNodeInputItemType } from '../workflow/type/io';
import type { FlowNodeTemplateType } from '../workflow/type/node.d';
/* --------- chat ---------- */
export type ChatSchemaType = {
_id: string;
chatId: string;
@ -49,6 +50,7 @@ export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
appId: AppSchema;
};
/* --------- chat item ---------- */
export type UserChatItemValueItemType = {
type: ChatItemValueTypeEnum.text | ChatItemValueTypeEnum.file;
text?: {
@ -65,6 +67,7 @@ export type UserChatItemType = {
value: UserChatItemValueItemType[];
hideInUI?: boolean;
};
export type SystemChatItemValueItemType = {
type: ChatItemValueTypeEnum.text;
text?: {
@ -92,7 +95,6 @@ export type AIChatItemValueItemType = {
tools?: ToolModuleResponseItemType[];
interactive?: WorkflowInteractiveResponseType;
};
export type AIChatItemType = {
obj: ChatRoleEnum.AI;
value: AIChatItemValueItemType[];
@ -101,14 +103,22 @@ export type AIChatItemType = {
userBadFeedback?: string;
customFeedbacks?: string[];
adminFeedback?: AdminFbkType;
durationSeconds?: number;
errorMsg?: string;
citeCollectionIds?: string[];
// @deprecated 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema
[DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[];
};
export type ChatItemValueItemType =
| UserChatItemValueItemType
| SystemChatItemValueItemType
| AIChatItemValueItemType;
export type ChatItemMergeType = UserChatItemType | SystemChatItemType | AIChatItemType;
export type ChatItemSchema = (UserChatItemType | SystemChatItemType | AIChatItemType) & {
export type ChatItemSchema = ChatItemMergeType & {
dataId: string;
chatId: string;
userId: string;
@ -116,8 +126,6 @@ export type ChatItemSchema = (UserChatItemType | SystemChatItemType | AIChatItem
tmbId: string;
appId: string;
time: Date;
durationSeconds?: number;
errorMsg?: string;
};
export type AdminFbkType = {
@ -128,7 +136,6 @@ export type AdminFbkType = {
a?: string;
};
/* --------- chat item ---------- */
export type ResponseTagItemType = {
totalQuoteList?: SearchDataResponseItemType[];
llmModuleAccount?: number;
@ -136,12 +143,12 @@ export type ResponseTagItemType = {
toolCiteLinks?: ToolCiteLinksType[];
};
export type ChatItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & {
export type ChatItemType = ChatItemMergeType & {
dataId?: string;
} & ResponseTagItemType;
// Frontend type
export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & {
export type ChatSiteItemType = ChatItemMergeType & {
_id?: string;
dataId: string;
status: `${ChatStatusEnum}`;
@ -153,6 +160,16 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt
errorMsg?: string;
} & ChatBoxInputType &
ResponseTagItemType;
/* --------- chat item response ---------- */
export type ChatItemResponseSchemaType = {
teamId: string;
appId: string;
chatId: string;
chatItemDataId: string;
data: ChatHistoryItemResType;
};
/* --------- team chat --------- */
export type ChatAppListSchema = {
apps: AppType[];

View File

@ -85,10 +85,10 @@ export const getHistoryPreview = (
// Filter workflow public response
export const filterPublicNodeResponseData = ({
flowResponses = [],
nodeRespones = [],
responseDetail = false
}: {
flowResponses?: ChatHistoryItemResType[];
nodeRespones?: ChatHistoryItemResType[];
responseDetail?: boolean;
}) => {
const publicNodeMap: Record<string, any> = {
@ -98,19 +98,28 @@ export const filterPublicNodeResponseData = ({
[FlowNodeTypeEnum.pluginOutput]: true
};
const filedList = responseDetail
? ['quoteList', 'moduleType', 'pluginOutput', 'runningTime']
: ['moduleType', 'pluginOutput', 'runningTime'];
const filedMap: Record<string, boolean> = responseDetail
? {
quoteList: true,
moduleType: true,
pluginOutput: true,
runningTime: true
}
: {
moduleType: true,
pluginOutput: true,
runningTime: true
};
return flowResponses
return nodeRespones
.filter((item) => publicNodeMap[item.moduleType])
.map((item) => {
const obj: DispatchNodeResponseType = {};
for (let key in item) {
if (key === 'toolDetail' || key === 'pluginDetail') {
// @ts-ignore
obj[key] = filterPublicNodeResponseData({ flowResponses: item[key], responseDetail });
} else if (filedList.includes(key)) {
obj[key] = filterPublicNodeResponseData({ nodeRespones: item[key], responseDetail });
} else if (filedMap[key]) {
// @ts-ignore
obj[key] = item[key];
}
@ -211,38 +220,32 @@ export const mergeChatResponseData = (
return item;
});
let lastResponse: ChatHistoryItemResType | undefined = undefined;
let hasMerged = false;
const result: ChatHistoryItemResType[] = [];
const mergeMap = new Map<string, number>(); // mergeSignId -> result index
const firstPassResult = responseWithMergedPlugins.reduce<ChatHistoryItemResType[]>(
(acc, curr) => {
if (
lastResponse &&
lastResponse.mergeSignId &&
curr.mergeSignId === lastResponse.mergeSignId
) {
const concatResponse: ChatHistoryItemResType = {
...curr,
runningTime: +((lastResponse.runningTime || 0) + (curr.runningTime || 0)).toFixed(2),
totalPoints: (lastResponse.totalPoints || 0) + (curr.totalPoints || 0),
childTotalPoints: (lastResponse.childTotalPoints || 0) + (curr.childTotalPoints || 0),
toolDetail: [...(lastResponse.toolDetail || []), ...(curr.toolDetail || [])],
loopDetail: [...(lastResponse.loopDetail || []), ...(curr.loopDetail || [])],
pluginDetail: [...(lastResponse.pluginDetail || []), ...(curr.pluginDetail || [])]
};
hasMerged = true;
return [...acc.slice(0, -1), concatResponse];
} else {
lastResponse = curr;
return [...acc, curr];
for (const item of responseWithMergedPlugins) {
if (item.mergeSignId && mergeMap.has(item.mergeSignId)) {
// Merge with existing item
const existingIndex = mergeMap.get(item.mergeSignId)!;
const existing = result[existingIndex];
result[existingIndex] = {
...item,
runningTime: +((existing.runningTime || 0) + (item.runningTime || 0)).toFixed(2),
totalPoints: (existing.totalPoints || 0) + (item.totalPoints || 0),
childTotalPoints: (existing.childTotalPoints || 0) + (item.childTotalPoints || 0),
toolDetail: [...(existing.toolDetail || []), ...(item.toolDetail || [])],
loopDetail: [...(existing.loopDetail || []), ...(item.loopDetail || [])],
pluginDetail: [...(existing.pluginDetail || []), ...(item.pluginDetail || [])]
};
} else {
// Add new item
result.push(item);
if (item.mergeSignId) {
mergeMap.set(item.mergeSignId, result.length - 1);
}
},
[]
);
if (hasMerged && firstPassResult.length > 1) {
return mergeChatResponseData(firstPassResult);
}
}
return firstPassResult;
return result;
};

View File

@ -48,6 +48,7 @@ export type ChatDispatchProps = {
id: string; // May be the id of the system plug-in (cannot be used directly to look up the table)
teamId: string;
tmbId: string; // App tmbId
name: string;
isChildApp?: boolean;
};
runningUserInfo: {
@ -78,6 +79,7 @@ export type ChatDispatchProps = {
responseAllData?: boolean;
responseDetail?: boolean;
usageId?: string;
};
export type ModuleDispatchProps<T> = ChatDispatchProps & {

View File

@ -36,7 +36,8 @@ export const HttpNode468: FlowNodeTemplateType = {
selectValueTypeList: Object.values(WorkflowIOValueTypeEnum),
showDescription: false,
showDefaultValue: true
}
},
deprecated: true
},
{
key: NodeInputKeyEnum.httpMethod,

View File

@ -14,6 +14,8 @@ type InteractiveBasicType = {
memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages
toolCallId: string; // 记录对应 tool 的id用于后续交互节点可以替换掉 tool 的 response
};
usageId?: string;
};
type InteractiveNodeType = {

View File

@ -19,7 +19,12 @@ import { ChatNodeUsageType } from '../../../support/wallet/bill/type';
import { RuntimeNodeItemType } from '../runtime/type';
import { RuntimeEdgeItemType, StoreEdgeItemType } from './edge';
import { NextApiResponse } from 'next';
import type { AppDetailType, AppSchema, McpToolConfigType } from '../../app/type';
import type {
AppDetailType,
AppSchema,
McpToolConfigType,
HttpToolConfigType
} from '../../app/type';
import type { ParentIdType } from 'common/parentFolder/type';
import { AppTypeEnum } from '../../app/constants';
import type { WorkflowInteractiveResponseType } from '../template/system/interactive/type';
@ -46,6 +51,17 @@ export type NodeToolConfigType = {
description: string;
}[];
};
httpToolSet?: {
toolId: string;
baseUrl: string;
toolList: HttpToolConfigType[];
apiSchemaStr: string;
customHeaders: string;
headerSecret?: StoreSecretValueType;
};
httpTool?: {
toolId: string;
};
};
export type FlowNodeCommonType = {
@ -142,6 +158,7 @@ export type NodeTemplateListItemType = {
instructions?: string; // 使用说明
courseUrl?: string; // 教程链接
sourceMember?: SourceMember;
toolSource?: 'uploaded' | 'built-in'; // Plugin source type
};
export type NodeTemplateListType = {

View File

@ -27,7 +27,8 @@ import type {
ChatInputGuideConfigType,
AppChatConfigType,
AppAutoExecuteConfigType,
AppQGConfigType
AppQGConfigType,
AppSchema
} from '../app/type';
import { type EditorVariablePickerType } from '../../../web/components/common/Textarea/PromptEditor/type';
import {
@ -441,3 +442,24 @@ export const getPluginRunUserQuery = ({
})
};
};
export const removeUnauthModels = async ({
modules,
allowedModels = new Set()
}: {
modules: AppSchema['modules'];
allowedModels?: Set<string>;
}) => {
if (modules) {
modules.forEach((module) => {
module.inputs.forEach((input) => {
if (input.key === 'model') {
if (!allowedModels.has(input.value)) {
input.value = undefined;
}
}
});
});
}
return modules;
};

View File

@ -2,7 +2,7 @@
"name": "@fastgpt/global",
"version": "1.0.0",
"dependencies": {
"@fastgpt-sdk/plugin": "^0.1.16",
"@fastgpt-sdk/plugin": "^0.1.19",
"@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8",
"axios": "^1.12.1",

View File

@ -49,7 +49,8 @@ export const PermissionTypeMap = {
export enum PerResourceTypeEnum {
team = 'team',
app = 'app',
dataset = 'dataset'
dataset = 'dataset',
model = 'model'
}
/* new permission */

View File

@ -0,0 +1,11 @@
import {
NullRoleVal,
CommonPerKeyEnum,
CommonRoleList,
CommonPerList,
CommonRoleKeyEnum
} from '../constant';
export const ModelDefaultRole = NullRoleVal;
export const ModelReadPerVal = CommonPerList[CommonPerKeyEnum.read];
export const ModelReadRolVal = CommonRoleList[CommonRoleKeyEnum.read];

View File

@ -0,0 +1,3 @@
import { Permission } from '../controller';
export class ModelPermission extends Permission {}

View File

@ -1,8 +1,8 @@
import type { UserModelSchema } from '../user/type';
import type { RequireOnlyOne } from '../../common/type/utils';
import type { TeamMemberSchema } from '../user/team/type';
import type { CommonRoleKeyEnum } from './constant';
import { type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant';
import type { CollaboratorIdType } from './collaborator';
// PermissionValueType, the type of permission's value is a number, which is a bit field actually.
// It is spired by the permission system in Linux.
@ -63,11 +63,8 @@ export type ResourcePermissionType = {
resourceType: ResourceType;
permission: PermissionValueType;
resourceId: string;
} & RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;
resourceName: string;
} & CollaboratorIdType;
export type ResourcePerWithTmbWithUser = Omit<ResourcePermissionType, 'tmbId'> & {
tmbId: TeamMemberSchema & { user: UserModelSchema };

View File

@ -1,5 +1,5 @@
import type { UsageSourceEnum } from './constants';
import type { UsageListItemCountType, UsageListItemType } from './type';
import type { UsageItemTypeEnum, UsageSourceEnum } from './constants';
import type { UsageItemCountType, UsageItemType, UsageListItemType, UsageSchemaType } from './type';
export type CreateTrainingUsageProps = {
name: string;
@ -22,21 +22,15 @@ export type GetUsageDashboardResponseItem = {
totalPoints: number;
};
export type ConcatUsageProps = UsageListItemCountType & {
export type CreateUsageProps = Omit<UsageSchemaType, '_id' | 'time'>;
export type ConcatUsageProps = {
teamId: string;
tmbId: string;
billId?: string;
usageId: string;
totalPoints: number;
listIndex?: number;
};
export type CreateUsageProps = {
itemType: UsageItemTypeEnum;
} & UsageItemCountType;
export type PushUsageItemsProps = {
teamId: string;
tmbId: string;
appName: string;
appId?: string;
pluginId?: string;
totalPoints: number;
source: `${UsageSourceEnum}`;
list: UsageListItemType[];
usageId: string;
list: UsageItemType[];
};

View File

@ -14,7 +14,8 @@ export enum UsageSourceEnum {
pdfParse = 'pdfParse',
mcp = 'mcp',
evaluation = 'evaluation',
optimize_prompt = 'optimize_prompt'
optimize_prompt = 'optimize_prompt',
code_copilot = 'code_copilot'
}
export const UsageSourceMap = {
@ -59,5 +60,20 @@ export const UsageSourceMap = {
},
[UsageSourceEnum.optimize_prompt]: {
label: i18nT('common:support.wallet.usage.Optimize Prompt')
},
[UsageSourceEnum.code_copilot]: {
label: i18nT('common:support.wallet.usage.Code Copilot')
}
};
export enum UsageItemTypeEnum {
training_vector = 1,
training_qa = 2,
training_autoIndex = 3,
training_imageIndex = 4,
training_paragraph = 5,
training_imageParse = 6,
evaluation_generateAnswer = 7,
evaluation_answerAccuracy = 8
}

View File

@ -1,8 +1,33 @@
import type { SourceMemberType } from '../../../support/user/type';
import type { CreateUsageProps } from './api';
import { UsageSourceEnum } from './constants';
import type { UsageItemTypeEnum, UsageSourceEnum } from './constants';
export type UsageListItemCountType = {
export type UsageSchemaType = {
_id: string;
time: Date;
teamId: string;
tmbId: string;
appName: string;
appId?: string;
pluginId?: string;
totalPoints: number;
source: `${UsageSourceEnum}`;
// @deprecated
list?: UsageItemType[];
};
export type UsageItemSchemaType = {
_id: string;
teamId: string;
usageId: string;
name: string;
amount: number;
time: Date;
itemType?: UsageItemTypeEnum; // Use in usage concat
} & UsageItemCountType;
export type UsageItemCountType = {
model?: string;
inputTokens?: number;
outputTokens?: number;
charsLength?: number;
@ -14,24 +39,18 @@ export type UsageListItemCountType = {
tokens?: number;
};
export type UsageListItemType = UsageListItemCountType & {
export type UsageItemType = UsageItemCountType & {
moduleName: string;
amount: number;
model?: string;
count?: number;
itemType?: UsageItemTypeEnum;
};
export type UsageSchemaType = CreateUsageProps & {
_id: string;
time: Date;
};
export type UsageItemType = {
export type UsageListItemType = {
id: string;
time: Date;
appName: string;
source: UsageSchemaType['source'];
totalPoints: number;
list: UsageSchemaType['list'];
list: Omit<UsageItemType, 'itemType'>[];
sourceMember: SourceMemberType;
};

View File

@ -8,7 +8,11 @@ import type {
SearchDatasetDataResponse
} from '../../core/dataset/search/controller';
import type { AuthOpenApiLimitProps } from '../../support/openapi/auth';
import type { CreateUsageProps, ConcatUsageProps } from '@fastgpt/global/support/wallet/usage/api';
import type {
CreateUsageProps,
ConcatUsageProps,
PushUsageItemsProps
} from '@fastgpt/global/support/wallet/usage/api';
declare global {
var textCensorHandler: (params: { text: string }) => Promise<{ code: number; message?: string }>;
@ -16,4 +20,5 @@ declare global {
var authOpenApiHandler: (data: AuthOpenApiLimitProps) => Promise<any>;
var createUsageHandler: (data: CreateUsageProps) => any;
var concatUsageHandler: (data: ConcatUsageProps) => any;
var pushUsageItemsHandler: (data: PushUsageItemsProps) => any;
}

60
packages/service/common/cache/index.ts vendored Normal file
View File

@ -0,0 +1,60 @@
import './init';
import { getGlobalRedisConnection } from '../../common/redis';
import type { SystemCacheKeyEnum } from './type';
import { randomUUID } from 'node:crypto';
import { initCache } from './init';
const cachePrefix = `VERSION_KEY:`;
/**
*
* @param key SystemCacheKeyEnum
* @param id string (teamId, tmbId, etc), if '*' is used, all keys will be refreshed
*/
export const refreshVersionKey = async (key: `${SystemCacheKeyEnum}`, id?: string | '*') => {
const redis = getGlobalRedisConnection();
if (!global.systemCache) initCache();
const val = randomUUID();
const versionKey = id ? `${cachePrefix}${key}:${id}` : `${cachePrefix}${key}`;
if (id === '*') {
const pattern = `${cachePrefix}${key}:*`;
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(keys);
}
} else {
await redis.set(versionKey, val);
}
};
export const getVersionKey = async (key: `${SystemCacheKeyEnum}`, id?: string) => {
const redis = getGlobalRedisConnection();
if (!global.systemCache) initCache();
const versionKey = id ? `${cachePrefix}${key}:${id}` : `${cachePrefix}${key}`;
const val = await redis.get(versionKey);
if (val) return val;
// if there is no val set to the key, init a new val.
const initVal = randomUUID();
await redis.set(versionKey, initVal);
return initVal;
};
export const getCachedData = async <T extends SystemCacheKeyEnum>(key: T, id?: string) => {
if (!global.systemCache) initCache();
const versionKey = await getVersionKey(key, id);
const isDisableCache = process.env.DISABLE_CACHE === 'true';
// 命中缓存
if (global.systemCache[key].versionKey === versionKey && !isDisableCache) {
return global.systemCache[key].data;
}
const refreshedData = await global.systemCache[key].refreshFunc();
global.systemCache[key].data = refreshedData;
global.systemCache[key].versionKey = versionKey;
return global.systemCache[key].data;
};

17
packages/service/common/cache/init.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import { SystemCacheKeyEnum } from './type';
import { refreshSystemTools } from '../../core/app/plugin/controller';
export const initCache = () => {
global.systemCache = {
[SystemCacheKeyEnum.systemTool]: {
versionKey: '',
data: [],
refreshFunc: refreshSystemTools
},
[SystemCacheKeyEnum.modelPermission]: {
versionKey: '',
data: null,
refreshFunc: () => Promise.resolve(null)
}
};
};

23
packages/service/common/cache/type.ts vendored Normal file
View File

@ -0,0 +1,23 @@
import type { SystemPluginTemplateItemType } from '@fastgpt/global/core/app/plugin/type';
export enum SystemCacheKeyEnum {
systemTool = 'systemTool',
modelPermission = 'modelPermission'
}
export type SystemCacheDataType = {
[SystemCacheKeyEnum.systemTool]: SystemPluginTemplateItemType[];
[SystemCacheKeyEnum.modelPermission]: null;
};
type SystemCacheType = {
[K in SystemCacheKeyEnum]: {
versionKey: string;
data: SystemCacheDataType[K];
refreshFunc: () => Promise<SystemCacheDataType[K]>;
};
};
declare global {
var systemCache: SystemCacheType;
}

View File

@ -196,7 +196,8 @@ export const readFileContentFromMongo = async ({
bucketName,
fileId,
customPdfParse = false,
getFormatText
getFormatText,
usageId
}: {
teamId: string;
tmbId: string;
@ -204,6 +205,7 @@ export const readFileContentFromMongo = async ({
fileId: string;
customPdfParse?: boolean;
getFormatText?: boolean; // 数据类型都尽可能转化成 markdown 格式
usageId?: string;
}): Promise<{
rawText: string;
filename: string;
@ -237,6 +239,7 @@ export const readFileContentFromMongo = async ({
// Get raw text
const { rawText } = await readRawContentByFileBuffer({
customPdfParse,
usageId,
getFormatText,
extension,
teamId,

View File

@ -47,6 +47,7 @@ export const readRawContentByFileBuffer = async ({
encoding,
metadata,
customPdfParse = false,
usageId,
getFormatText = true
}: {
teamId: string;
@ -58,6 +59,7 @@ export const readRawContentByFileBuffer = async ({
metadata?: Record<string, any>;
customPdfParse?: boolean;
usageId?: string;
getFormatText?: boolean;
}): Promise<{
rawText: string;
@ -104,7 +106,8 @@ export const readRawContentByFileBuffer = async ({
createPdfParseUsage({
teamId,
tmbId,
pages: response.pages
pages: response.pages,
usageId
});
return {
@ -123,7 +126,8 @@ export const readRawContentByFileBuffer = async ({
createPdfParseUsage({
teamId,
tmbId,
pages
pages,
usageId
});
return {

View File

@ -0,0 +1,10 @@
import type { S3ServiceConfig } from './type';
export const defualtS3Config: Omit<S3ServiceConfig, 'bucket'> = {
endPoint: process.env.S3_ENDPOINT || 'localhost',
port: process.env.S3_PORT ? parseInt(process.env.S3_PORT) : 9000,
useSSL: process.env.S3_USE_SSL === 'true',
accessKey: process.env.S3_ACCESS_KEY || 'minioadmin',
secretKey: process.env.S3_SECRET_KEY || 'minioadmin',
externalBaseURL: process.env.S3_EXTERNAL_BASE_URL
};

View File

@ -0,0 +1,20 @@
export const mimeMap: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.json': 'application/json',
'.csv': 'text/csv',
'.zip': 'application/zip',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.doc': 'application/msword',
'.xls': 'application/vnd.ms-excel',
'.ppt': 'application/vnd.ms-powerpoint',
'.js': 'application/javascript'
};

View File

@ -0,0 +1,168 @@
import { Client } from 'minio';
import {
type FileMetadataType,
type PresignedUrlInput as UploadPresignedURLProps,
type UploadPresignedURLResponse,
type S3ServiceConfig
} from './type';
import { defualtS3Config } from './config';
import { randomBytes } from 'crypto';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { extname } from 'path';
import { addLog } from '../../common/system/log';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { mimeMap } from './const';
export class S3Service {
private client: Client;
private config: S3ServiceConfig;
private initialized: boolean = false;
initFunction?: () => Promise<any>;
constructor(config?: Partial<S3ServiceConfig>) {
this.config = { ...defualtS3Config, ...config } as S3ServiceConfig;
this.client = new Client({
endPoint: this.config.endPoint,
port: this.config.port,
useSSL: this.config.useSSL,
accessKey: this.config.accessKey,
secretKey: this.config.secretKey,
transportAgent: process.env.HTTP_PROXY
? new HttpProxyAgent(process.env.HTTP_PROXY)
: process.env.HTTPS_PROXY
? new HttpsProxyAgent(process.env.HTTPS_PROXY)
: undefined
});
this.initFunction = config?.initFunction;
}
public async init() {
if (!this.initialized) {
if (!(await this.client.bucketExists(this.config.bucket))) {
addLog.debug(`Creating bucket: ${this.config.bucket}`);
await this.client.makeBucket(this.config.bucket);
}
await this.initFunction?.();
this.initialized = true;
}
}
private generateFileId(): string {
return randomBytes(16).toString('hex');
}
private generateAccessUrl(filename: string): string {
const protocol = this.config.useSSL ? 'https' : 'http';
const port =
this.config.port && this.config.port !== (this.config.useSSL ? 443 : 80)
? `:${this.config.port}`
: '';
const externalBaseURL = this.config.externalBaseURL;
return externalBaseURL
? `${externalBaseURL}/${this.config.bucket}/${encodeURIComponent(filename)}`
: `${protocol}://${this.config.endPoint}${port}/${this.config.bucket}/${encodeURIComponent(filename)}`;
}
uploadFile = async (fileBuffer: Buffer, originalFilename: string): Promise<FileMetadataType> => {
await this.init();
const inferContentType = (filename: string) => {
const ext = extname(filename).toLowerCase();
return mimeMap[ext] || 'application/octet-stream';
};
if (this.config.maxFileSize && fileBuffer.length > this.config.maxFileSize) {
return Promise.reject(
`File size ${fileBuffer.length} exceeds limit ${this.config.maxFileSize}`
);
}
const fileId = this.generateFileId();
const objectName = `${fileId}-${originalFilename}`;
const uploadTime = new Date();
const contentType = inferContentType(originalFilename);
await this.client.putObject(this.config.bucket, objectName, fileBuffer, fileBuffer.length, {
'Content-Type': contentType,
'Content-Disposition': `attachment; filename="${encodeURIComponent(originalFilename)}"`,
'x-amz-meta-original-filename': encodeURIComponent(originalFilename),
'x-amz-meta-upload-time': uploadTime.toISOString()
});
const metadata: FileMetadataType = {
fileId,
originalFilename,
contentType,
size: fileBuffer.length,
uploadTime,
accessUrl: this.generateAccessUrl(objectName)
};
return metadata;
};
generateUploadPresignedURL = async ({
filepath,
contentType,
metadata,
filename
}: UploadPresignedURLProps): Promise<UploadPresignedURLResponse> => {
await this.init();
const objectName = `${filepath}/${filename}`;
try {
const policy = this.client.newPostPolicy();
policy.setBucket(this.config.bucket);
policy.setKey(objectName);
if (contentType) {
policy.setContentType(contentType);
}
if (this.config.maxFileSize) {
policy.setContentLengthRange(1, this.config.maxFileSize);
}
policy.setExpires(new Date(Date.now() + 10 * 60 * 1000)); // 10 mins
policy.setUserMetaData({
'original-filename': encodeURIComponent(filename),
'upload-time': new Date().toISOString(),
...metadata
});
const { postURL, formData } = await this.client.presignedPostPolicy(policy);
const response: UploadPresignedURLResponse = {
objectName,
uploadUrl: postURL,
formData
};
return response;
} catch (error) {
addLog.error('Failed to generate Upload Presigned URL', error);
return Promise.reject(`Failed to generate Upload Presigned URL: ${getErrText(error)}`);
}
};
generateDownloadUrl = (objectName: string): string => {
const pathParts = objectName.split('/');
const encodedParts = pathParts.map((part) => encodeURIComponent(part));
const encodedObjectName = encodedParts.join('/');
return `${this.config.bucket}/${encodedObjectName}`;
};
getFile = async (objectName: string): Promise<string> => {
const stat = await this.client.statObject(this.config.bucket, objectName);
if (stat.size > 0) {
const accessUrl = this.generateDownloadUrl(objectName);
return accessUrl;
}
return Promise.reject(`File ${objectName} not found`);
};
}

View File

@ -0,0 +1,16 @@
import { S3Service } from './controller';
export const PluginS3Service = (() => {
if (!global.pluginS3Service) {
global.pluginS3Service = new S3Service({
bucket: process.env.S3_PLUGIN_BUCKET || 'fastgpt-plugin',
maxFileSize: 50 * 1024 * 1024 // 50MB
});
}
return global.pluginS3Service;
})();
declare global {
var pluginS3Service: S3Service;
}

View File

@ -0,0 +1,49 @@
import type { ClientOptions } from 'minio';
export type S3ServiceConfig = {
bucket: string;
externalBaseURL?: string;
/**
* Unit: Byte
*/
maxFileSize?: number;
/**
* for executing some init function for the s3 service
*/
initFunction?: () => Promise<any>;
} & ClientOptions;
export type FileMetadataType = {
fileId: string;
originalFilename: string;
contentType: string;
size: number;
uploadTime: Date;
accessUrl: string;
};
export type PresignedUrlInput = {
filepath: string;
filename: string;
contentType?: string;
metadata?: Record<string, string>;
};
export type UploadPresignedURLResponse = {
objectName: string;
uploadUrl: string;
formData: Record<string, string>;
};
export type FileUploadInput = {
buffer: Buffer;
filename: string;
};
export enum PluginTypeEnum {
tool = 'tool'
}
export const PluginFilePath = {
[PluginTypeEnum.tool]: 'plugin/tools'
};

View File

@ -2,3 +2,7 @@ export const FastGPTProUrl = process.env.PRO_URL ? `${process.env.PRO_URL}/api`
export const FastGPTPluginUrl = process.env.PLUGIN_BASE_URL ? `${process.env.PLUGIN_BASE_URL}` : '';
// @ts-ignore
export const isFastGPTProService = () => !!global.systemConfig;
export const isProVersion = () => {
return !!global.feConfigs?.isPlus;
};

View File

@ -2,12 +2,8 @@
import { PgVectorCtrl } from './pg';
import { ObVectorCtrl } from './oceanbase';
import { getVectorsByText } from '../../core/ai/embedding';
import type {
EmbeddingRecallCtrlProps} from './controller.d';
import {
type DelDatasetVectorCtrlProps,
type InsertVectorProps
} from './controller.d';
import type { EmbeddingRecallCtrlProps } from './controller.d';
import { type DelDatasetVectorCtrlProps, type InsertVectorProps } from './controller.d';
import { type EmbeddingModelItemType } from '@fastgpt/global/core/ai/model.d';
import { MILVUS_ADDRESS, PG_ADDRESS, OCEANBASE_ADDRESS } from './constants';
import { MilvusCtrl } from './milvus';

View File

@ -19,6 +19,8 @@ import { delay } from '@fastgpt/global/common/system/utils';
import { pluginClient } from '../../../thirdProvider/fastgptPlugin';
import { setCron } from '../../../common/system/cron';
import { preloadModelProviders } from '../../../core/app/provider/controller';
import { refreshVersionKey } from '../../../common/cache';
import { SystemCacheKeyEnum } from '../../../common/cache/type';
export const loadSystemModels = async (init = false, language = 'en') => {
const pushModel = (model: SystemModelItemType) => {
@ -253,6 +255,7 @@ export const updatedReloadSystemModel = async () => {
await loadSystemModels(true);
// 2. 更新缓存(仅主节点触发)
await updateFastGPTConfigBuffer();
await refreshVersionKey(SystemCacheKeyEnum.modelPermission, '*');
// 3. 延迟1秒等待其他节点刷新
await delay(1000);
};

View File

@ -142,8 +142,8 @@ export const createLLMResponse = async <T extends CompletionsBodyType>(
// Usage count
const inputTokens =
usage?.prompt_tokens ?? (await countGptMessagesTokens(requestBody.messages, requestBody.tools));
const outputTokens = usage?.completion_tokens ?? (await countGptMessagesTokens(assistantMessage));
usage?.prompt_tokens || (await countGptMessagesTokens(requestBody.messages, requestBody.tools));
const outputTokens = usage?.completion_tokens || (await countGptMessagesTokens(assistantMessage));
return {
isStreamResponse,
@ -645,4 +645,4 @@ const createChatCompletion = async ({
}
return Promise.reject(error);
}
};
};

View File

@ -23,6 +23,7 @@ import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant
import { removeImageByPath } from '../../common/file/image/controller';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { MongoAppLogKeys } from './logs/logkeysSchema';
import { MongoChatItemResponse } from '../chat/chatItemResponseSchema';
export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => {
if (!nodes) return;
@ -159,6 +160,9 @@ export const onDelOneApp = async ({
// Delete chats
await deleteChatFiles({ appId });
await MongoChatItemResponse.deleteMany({
appId
});
await MongoChatItem.deleteMany({
appId
});

View File

@ -5,7 +5,7 @@ import {
import { connectionMongo, getMongoModel } from '../../../common/mongo';
import { AppCollectionName } from '../schema';
import type { EvaluationSchemaType } from '@fastgpt/global/core/app/evaluation/type';
import { UsageCollectionName } from '../../../support/wallet/usage/schema';
import { UsageCollectionName } from '../../../support/wallet/usage/constants';
const { Schema } = connectionMongo;
export const EvaluationCollectionName = 'eval';

View File

@ -0,0 +1,58 @@
import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import { getSecretValue } from '../../common/secret/utils';
import axios from 'axios';
import { getErrText } from '@fastgpt/global/common/error/utils';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type RunHTTPToolParams = {
baseUrl: string;
toolPath: string;
method: string;
params: Record<string, any>;
headerSecret?: StoreSecretValueType;
customHeaders?: Record<string, string>;
};
export type RunHTTPToolResult = RequireOnlyOne<{
data?: any;
errorMsg?: string;
}>;
export async function runHTTPTool({
baseUrl,
toolPath,
method = 'POST',
params,
headerSecret,
customHeaders
}: RunHTTPToolParams): Promise<RunHTTPToolResult> {
try {
const headers = {
'Content-Type': 'application/json',
...(customHeaders || {}),
...(headerSecret ? getSecretValue({ storeSecret: headerSecret }) : {})
};
const { data } = await axios({
method: method.toUpperCase(),
baseURL: baseUrl.startsWith('https://') ? baseUrl : `https://${baseUrl}`,
url: toolPath,
headers,
data: params,
params,
timeout: 300000,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
return {
data
};
} catch (error: any) {
console.log(error);
return {
errorMsg: getErrText(error)
};
}
}

View File

@ -43,10 +43,13 @@ import { isProduction } from '@fastgpt/global/common/system/constants';
import { Output_Template_Error_Message } from '@fastgpt/global/core/workflow/template/output';
import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils';
import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils';
import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/httpTools/utils';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getMCPChildren } from '../mcp';
import { cloneDeep } from 'lodash';
import { UserError } from '@fastgpt/global/common/error/utils';
import { getCachedData } from '../../../common/cache';
import { SystemCacheKeyEnum } from '../../../common/cache/type';
type ChildAppType = SystemPluginTemplateItemType & {
teamId?: string;
@ -56,6 +59,8 @@ type ChildAppType = SystemPluginTemplateItemType & {
isLatestVersion?: boolean; // Auto computed
};
export const getSystemTools = () => getCachedData(SystemCacheKeyEnum.systemTool);
export const getSystemPluginByIdAndVersionId = async (
pluginId: string,
versionId?: string
@ -190,11 +195,11 @@ export async function getChildAppPreviewNode({
mcpToolSet: {
toolId: pluginId,
toolList: children,
url: ''
url: '',
headerSecret: {}
}
};
}
return {
id: String(item._id),
teamId: String(item.teamId),
@ -260,6 +265,45 @@ export async function getChildAppPreviewNode({
isLatestVersion: true
};
}
// http tool
else if (source === PluginSourceEnum.http) {
const [parentId, toolName] = pluginId.split('/');
const item = await MongoApp.findById(parentId).lean();
if (!item) return Promise.reject(PluginErrEnum.unExist);
const version = await getAppVersionById({ appId: parentId, versionId, app: item });
const toolConfig = version.nodes[0].toolConfig?.httpToolSet;
const tool = await (async () => {
if (toolConfig?.toolList) {
return toolConfig.toolList.find((item) => item.name === toolName);
}
return undefined;
})();
if (!tool) return Promise.reject(PluginErrEnum.unExist);
return {
avatar: item.avatar,
id: appId,
name: tool.name,
templateType: FlowNodeTemplateTypeEnum.tools,
workflow: {
nodes: [
getHTTPToolRuntimeNode({
tool: {
description: tool.description,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
name: `${item.name}/${tool.name}`
},
avatar: item.avatar,
parentId: item._id
})
],
edges: []
},
version: '',
isLatestVersion: true
};
}
// 1. System Tools
// 2. System Plugins configured in Pro (has associatedPluginId)
else {
@ -503,97 +547,63 @@ const dbPluginFormat = (item: SystemPluginConfigSchemaType): SystemPluginTemplat
};
/* FastsGPT-Pluign api: */
function getCachedSystemPlugins() {
if (!global.systemToolsCache) {
global.systemToolsCache = {
expires: 0,
data: [] as SystemPluginTemplateItemType[]
export const refreshSystemTools = async (): Promise<SystemPluginTemplateItemType[]> => {
const tools = await APIGetSystemToolList();
// 从数据库里加载插件配置进行替换
const systemToolsArray = await MongoSystemPlugin.find({}).lean();
const systemTools = new Map(systemToolsArray.map((plugin) => [plugin.pluginId, plugin]));
const formatTools = tools.map<SystemPluginTemplateItemType>((item) => {
const dbPluginConfig = systemTools.get(item.id);
const isFolder = tools.some((tool) => tool.parentId === item.id);
const versionList = (item.versionList as SystemPluginTemplateItemType['versionList']) || [];
return {
id: item.id,
parentId: item.parentId,
isFolder,
name: item.name,
avatar: item.avatar,
intro: item.description,
toolDescription: item.toolDescription,
author: item.author,
courseUrl: item.courseUrl,
instructions: dbPluginConfig?.customConfig?.userGuide,
weight: item.weight,
toolSource: item.toolSource || 'built-in',
workflow: {
nodes: [],
edges: []
},
versionList,
templateType: item.templateType,
showStatus: true,
isActive: dbPluginConfig?.isActive ?? item.isActive ?? true,
inputList: item?.secretInputConfig,
hasSystemSecret: !!dbPluginConfig?.inputListVal,
originCost: dbPluginConfig?.originCost ?? 0,
currentCost: dbPluginConfig?.currentCost ?? 0,
systemKeyCost: dbPluginConfig?.systemKeyCost ?? 0,
hasTokenFee: dbPluginConfig?.hasTokenFee ?? false,
pluginOrder: dbPluginConfig?.pluginOrder
};
}
return global.systemToolsCache;
}
const cleanSystemPluginCache = () => {
global.systemToolsCache = undefined;
};
export const refetchSystemPlugins = () => {
const changeStream = MongoSystemPlugin.watch();
changeStream.on('change', () => {
try {
cleanSystemPluginCache();
} catch (error) {}
});
};
export const getSystemTools = async (): Promise<SystemPluginTemplateItemType[]> => {
if (getCachedSystemPlugins().expires > Date.now() && isProduction) {
return getCachedSystemPlugins().data;
} else {
const tools = await APIGetSystemToolList();
const dbPlugins = systemToolsArray
.filter((item) => item.customConfig?.associatedPluginId)
.map((item) => dbPluginFormat(item));
// 从数据库里加载插件配置进行替换
const systemToolsArray = await MongoSystemPlugin.find({}).lean();
const systemTools = new Map(systemToolsArray.map((plugin) => [plugin.pluginId, plugin]));
const concatTools = [...formatTools, ...dbPlugins];
concatTools.sort((a, b) => (a.pluginOrder ?? 0) - (b.pluginOrder ?? 0));
const formatTools = tools.map<SystemPluginTemplateItemType>((item) => {
const dbPluginConfig = systemTools.get(item.id);
const isFolder = tools.some((tool) => tool.parentId === item.id);
global.systemToolsTypeCache = {};
concatTools.forEach((item) => {
global.systemToolsTypeCache[item.templateType] = 1;
});
const versionList = (item.versionList as SystemPluginTemplateItemType['versionList']) || [];
return {
id: item.id,
parentId: item.parentId,
isFolder,
name: item.name,
avatar: item.avatar,
intro: item.description,
toolDescription: item.toolDescription,
author: item.author,
courseUrl: item.courseUrl,
instructions: dbPluginConfig?.customConfig?.userGuide,
weight: item.weight,
workflow: {
nodes: [],
edges: []
},
versionList,
templateType: item.templateType,
showStatus: true,
isActive: dbPluginConfig?.isActive ?? item.isActive ?? true,
inputList: item?.secretInputConfig,
hasSystemSecret: !!dbPluginConfig?.inputListVal,
originCost: dbPluginConfig?.originCost ?? 0,
currentCost: dbPluginConfig?.currentCost ?? 0,
systemKeyCost: dbPluginConfig?.systemKeyCost ?? 0,
hasTokenFee: dbPluginConfig?.hasTokenFee ?? false,
pluginOrder: dbPluginConfig?.pluginOrder
};
});
// TODO: Check the app exists
const dbPlugins = systemToolsArray
.filter((item) => item.customConfig?.associatedPluginId)
.map((item) => dbPluginFormat(item));
const concatTools = [...formatTools, ...dbPlugins];
concatTools.sort((a, b) => (a.pluginOrder ?? 0) - (b.pluginOrder ?? 0));
global.systemToolsCache = {
expires: Date.now() + 30 * 60 * 1000, // 30 minutes
data: concatTools
};
global.systemToolsTypeCache = {};
concatTools.forEach((item) => {
global.systemToolsTypeCache[item.templateType] = 1;
});
return concatTools;
}
return concatTools;
};
export const getSystemToolById = async (id: string): Promise<SystemPluginTemplateItemType> => {
@ -613,11 +623,5 @@ export const getSystemToolById = async (id: string): Promise<SystemPluginTemplat
};
declare global {
var systemToolsCache:
| {
expires: number;
data: SystemPluginTemplateItemType[];
}
| undefined;
var systemToolsTypeCache: Record<string, 1>;
}

View File

@ -35,7 +35,7 @@ const getAppTemplates = async () => {
const res = [
...communityTemplateConfig,
...dbTemplates.filter((t) => !isCommunityTemplate(t.templateId))
...dbTemplates.filter((t) => isCommercialTemaplte(t.templateId))
].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return res;
@ -60,8 +60,8 @@ export const getAppTemplatesAndLoadThem = async (refresh = false) => {
}
};
export const isCommunityTemplate = (templateId: string) => {
return templateId.startsWith(PluginSourceEnum.community);
export const isCommercialTemaplte = (templateId: string) => {
return templateId.startsWith(PluginSourceEnum.commercial);
};
declare global {

View File

@ -1,7 +1,7 @@
import type { I18nStringStrictType, ToolTypeEnum } from '@fastgpt/global/sdk/fastgpt-plugin';
import { RunToolWithStream } from '@fastgpt/global/sdk/fastgpt-plugin';
import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants';
import { pluginClient, BASE_URL, TOKEN } from '../../../thirdProvider/fastgptPlugin';
import { pluginClient, PLUGIN_BASE_URL, PLUGIN_TOKEN } from '../../../thirdProvider/fastgptPlugin';
import { addLog } from '../../../common/system/log';
import { retryFn } from '@fastgpt/global/common/system/utils';
@ -26,8 +26,8 @@ export async function APIGetSystemToolList() {
}
const runToolInstance = new RunToolWithStream({
baseUrl: BASE_URL,
token: TOKEN
baseUrl: PLUGIN_BASE_URL,
token: PLUGIN_TOKEN
});
export const APIRunSystemTool = runToolInstance.run.bind(runToolInstance);

View File

@ -0,0 +1,47 @@
import { connectionMongo, getMongoModel } from '../../common/mongo';
const { Schema } = connectionMongo;
import type { ChatItemResponseSchemaType } from '@fastgpt/global/core/chat/type';
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { AppCollectionName } from '../app/schema';
import { ChatItemResponseCollectionName } from './constants';
const ChatItemResponseSchema = new Schema({
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
appId: {
type: Schema.Types.ObjectId,
ref: AppCollectionName,
required: true
},
chatId: {
type: String,
require: true
},
chatItemDataId: {
type: String,
require: true
},
data: {
type: Object,
default: {}
},
time: {
type: Date,
default: () => new Date()
}
});
// Get response/Delete
ChatItemResponseSchema.index({ appId: 1, chatId: 1, chatItemDataId: 1 });
// Clear expired response
ChatItemResponseSchema.index({ teamId: 1, time: -1 });
export const MongoChatItemResponse = getMongoModel<ChatItemResponseSchemaType>(
ChatItemResponseCollectionName,
ChatItemResponseSchema
);

View File

@ -10,8 +10,7 @@ import {
import { AppCollectionName } from '../app/schema';
import { userCollectionName } from '../../support/user/schema';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
export const ChatItemCollectionName = 'chatitems';
import { ChatItemCollectionName } from './constants';
const ChatItemSchema = new Schema({
teamId: {
@ -35,7 +34,7 @@ const ChatItemSchema = new Schema({
dataId: {
type: String,
require: true,
default: () => getNanoid(22)
default: () => getNanoid(24)
},
appId: {
type: Schema.Types.ObjectId,
@ -61,6 +60,8 @@ const ChatItemSchema = new Schema({
type: Array,
default: []
},
// Field memory
memories: Object,
errorMsg: String,
userGoodFeedback: String,
@ -75,29 +76,25 @@ const ChatItemSchema = new Schema({
a: String
}
},
[DispatchNodeResponseKeyEnum.nodeResponse]: {
type: Array,
default: []
},
durationSeconds: Number
durationSeconds: Number,
citeCollectionIds: [String],
// @deprecated
[DispatchNodeResponseKeyEnum.nodeResponse]: Array
});
try {
ChatItemSchema.index({ dataId: 1 });
/* delete by app;
delete by chat id;
get chat list;
get chat logs;
close custom feedback;
*/
ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 });
// timer, clear history
ChatItemSchema.index({ teamId: 1, time: -1 });
/*
delete by app;
delete by chat id;
get chat list;
get chat logs;
close custom feedback;
*/
ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 });
// timer, clear history
ChatItemSchema.index({ teamId: 1, time: -1 });
// Admin charts
ChatItemSchema.index({ obj: 1, time: -1 }, { partialFilterExpression: { obj: 'Human' } });
} catch (error) {
console.log(error);
}
// Admin charts
ChatItemSchema.index({ obj: 1, time: -1 }, { partialFilterExpression: { obj: 'Human' } });
export const MongoChatItem = getMongoModel<ChatItemType>(ChatItemCollectionName, ChatItemSchema);

View File

@ -7,8 +7,7 @@ import {
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { AppCollectionName } from '../app/schema';
export const chatCollectionName = 'chat';
import { chatCollectionName } from './constants';
const ChatSchema = new Schema({
chatId: {

View File

@ -0,0 +1,3 @@
export const chatCollectionName = 'chats';
export const ChatItemResponseCollectionName = 'chat_item_responses';
export const ChatItemCollectionName = 'chatitems';

View File

@ -1,10 +1,13 @@
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import type { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/type';
import { MongoChatItem } from './chatItemSchema';
import { addLog } from '../../common/system/log';
import { delFileByFileIdList, getGFSCollection } from '../../common/file/gridfs/controller';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
import { MongoChat } from './chatSchema';
import { UserError } from '@fastgpt/global/common/error/utils';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { MongoChatItemResponse } from './chatItemResponseSchema';
export async function getChatItems({
appId,
@ -23,12 +26,44 @@ export async function getChatItems({
return { histories: [], total: 0 };
}
// Extend dataId
field = `dataId ${field}`;
const [histories, total] = await Promise.all([
MongoChatItem.find({ chatId, appId }, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(),
MongoChatItem.countDocuments({ chatId, appId })
MongoChatItem.find({ appId, chatId }, field).sort({ _id: -1 }).skip(offset).limit(limit).lean(),
MongoChatItem.countDocuments({ appId, chatId })
]);
histories.reverse();
// Add node responses field
if (field.includes(DispatchNodeResponseKeyEnum.nodeResponse)) {
const chatItemDataIds = histories
.filter((item) => item.obj === ChatRoleEnum.AI && !item.responseData?.length)
.map((item) => item.dataId);
const chatItemResponsesMap = await MongoChatItemResponse.find(
{ appId, chatId, chatItemDataId: { $in: chatItemDataIds } },
{ chatItemDataId: 1, data: 1 }
)
.lean()
.then((res) => {
const map = new Map<string, ChatHistoryItemResType[]>();
res.forEach((item) => {
const val = map.get(item.chatItemDataId) || [];
val.push(item.data);
map.set(item.chatItemDataId, val);
});
return map;
});
histories.forEach((item) => {
const val = chatItemResponsesMap.get(String(item.dataId));
if (item.obj === ChatRoleEnum.AI && val) {
item.responseData = val;
}
});
}
return { histories, total };
}

View File

@ -2,11 +2,7 @@ import { addLog } from '../../common/system/log';
import { MongoChatItem } from './chatItemSchema';
import { MongoChat } from './chatSchema';
import axios from 'axios';
import {
type AIChatItemType,
type ChatItemType,
type UserChatItemType
} from '@fastgpt/global/core/chat/type';
import { type AIChatItemType, type UserChatItemType } from '@fastgpt/global/core/chat/type';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
export type Metadata = {
@ -43,18 +39,6 @@ export const pushChatLog = ({
}
};
type ChatItem = ChatItemType & {
userGoodFeedback?: string;
userBadFeedback?: string;
chatId: string;
responseData: {
moduleType: string;
runningTime: number; //s
historyPreview: { obj: string; value: string }[];
}[];
time: Date;
};
type ChatLog = {
title: string;
feedback: 'like' | 'dislike' | null;

View File

@ -12,10 +12,11 @@ import { type AppChatConfigType } from '@fastgpt/global/core/app/type';
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { pushChatLog } from './pushChatLog';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { MongoAppChatLog } from '../app/logs/chatLogsSchema';
import { writePrimary } from '../../common/mongo/utils';
import { MongoChatItemResponse } from './chatItemResponseSchema';
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
type Props = {
chatId: string;
@ -31,12 +32,59 @@ type Props = {
sourceName?: string;
shareId?: string;
outLinkUid?: string;
content: [UserChatItemType & { dataId?: string }, AIChatItemType & { dataId?: string }];
userContent: UserChatItemType & { dataId?: string };
aiContent: AIChatItemType & { dataId?: string };
metadata?: Record<string, any>;
durationSeconds: number; //s
errorMsg?: string;
};
const formatAiContent = ({
aiContent,
durationSeconds,
errorMsg
}: {
aiContent: AIChatItemType & { dataId?: string };
durationSeconds: number;
errorMsg?: string;
}) => {
const { responseData, ...aiResponse } = aiContent;
const citeCollectionIds = new Set<string>();
const nodeResponses = responseData?.map((responseItem) => {
if (responseItem.moduleType === FlowNodeTypeEnum.datasetSearchNode && responseItem.quoteList) {
return {
...responseItem,
quoteList: responseItem.quoteList.map((quote) => {
citeCollectionIds.add(quote.collectionId);
return {
id: quote.id,
chunkIndex: quote.chunkIndex,
datasetId: quote.datasetId,
collectionId: quote.collectionId,
sourceId: quote.sourceId,
sourceName: quote.sourceName,
score: quote.score
};
})
};
}
return responseItem;
});
return {
aiResponse: {
...aiResponse,
durationSeconds,
errorMsg,
citeCollectionIds: Array.from(citeCollectionIds)
},
nodeResponses,
citeCollectionIds
};
};
export async function saveChat({
chatId,
appId,
@ -51,7 +99,8 @@ export async function saveChat({
sourceName,
shareId,
outLinkUid,
content,
userContent,
aiContent,
durationSeconds,
errorMsg,
metadata = {}
@ -81,42 +130,15 @@ export async function saveChat({
)?.inputs;
// Format save chat content: Remove quote q/a
const processedContent = content.map((item) => {
if (item.obj === ChatRoleEnum.AI) {
const nodeResponse = item[DispatchNodeResponseKeyEnum.nodeResponse]?.map((responseItem) => {
if (
responseItem.moduleType === FlowNodeTypeEnum.datasetSearchNode &&
responseItem.quoteList
) {
return {
...responseItem,
quoteList: responseItem.quoteList.map((quote: any) => ({
id: quote.id,
chunkIndex: quote.chunkIndex,
datasetId: quote.datasetId,
collectionId: quote.collectionId,
sourceId: quote.sourceId,
sourceName: quote.sourceName,
score: quote.score,
tokens: quote.tokens
}))
};
}
return responseItem;
});
return {
...item,
[DispatchNodeResponseKeyEnum.nodeResponse]: nodeResponse,
durationSeconds,
errorMsg
};
}
return item;
const { aiResponse, nodeResponses } = formatAiContent({
aiContent,
durationSeconds,
errorMsg
});
const processedContent = [userContent, aiResponse];
await mongoSessionRun(async (session) => {
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.create(
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi, dataId }] = await MongoChatItem.create(
processedContent.map((item) => ({
chatId,
teamId,
@ -127,6 +149,20 @@ export async function saveChat({
{ session, ordered: true, ...writePrimary }
);
// Create chat item respones
if (nodeResponses) {
await MongoChatItemResponse.create(
nodeResponses.map((item) => ({
teamId,
appId,
chatId,
chatItemDataId: dataId,
data: item
})),
{ session, ordered: true, ...writePrimary }
);
}
await MongoChat.updateOne(
{
appId,
@ -166,18 +202,15 @@ export async function saveChat({
});
});
// Create chat data log
try {
const userId = String(outLinkUid || tmbId);
const now = new Date();
const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000);
const aiResponse = processedContent.find((item) => item.obj === ChatRoleEnum.AI);
const errorCount = aiResponse?.responseData?.some((item) => item.errorText) ? 1 : 0;
const errorCount = nodeResponses?.some((item) => item.errorText) ? 1 : 0;
const totalPoints =
aiResponse?.responseData?.reduce(
(sum: number, item: any) => sum + (item.totalPoints || 0),
0
) || 0;
nodeResponses?.reduce((sum: number, item: any) => sum + (item.totalPoints || 0), 0) || 0;
const hasHistoryChat = await MongoAppChatLog.exists({
teamId,
@ -242,20 +275,16 @@ export async function saveChat({
}
export const updateInteractiveChat = async ({
teamId,
chatId,
appId,
userInteractiveVal,
aiResponse,
newVariables,
durationSeconds
}: {
chatId: string;
appId: string;
userInteractiveVal: string;
aiResponse: AIChatItemType & { dataId?: string };
newVariables?: Record<string, any>;
durationSeconds: number;
}) => {
userContent,
aiContent,
variables,
durationSeconds,
errorMsg
}: Props) => {
if (!chatId) return;
const chatItem = await MongoChatItem.findOne({ appId, chatId, obj: ChatRoleEnum.AI }).sort({
@ -276,17 +305,23 @@ export const updateInteractiveChat = async ({
}
const parsedUserInteractiveVal = (() => {
const { text: userInteractiveVal } = chatValue2RuntimePrompt(userContent.value);
try {
return JSON.parse(userInteractiveVal);
} catch (err) {
return userInteractiveVal;
}
})();
const { aiResponse, nodeResponses } = formatAiContent({
aiContent,
durationSeconds,
errorMsg
});
let finalInteractive = extractDeepestInteractive(interactiveValue.interactive);
if (finalInteractive.type === 'userSelect') {
finalInteractive.params.userSelectedVal = userInteractiveVal;
finalInteractive.params.userSelectedVal = parsedUserInteractiveVal;
} else if (
finalInteractive.type === 'userInput' &&
typeof parsedUserInteractiveVal === 'object'
@ -308,16 +343,14 @@ export const updateInteractiveChat = async ({
? [...chatItem.customFeedbacks, ...aiResponse.customFeedbacks]
: aiResponse.customFeedbacks;
}
if (aiResponse.responseData) {
chatItem.responseData = chatItem.responseData
? mergeChatResponseData([...chatItem.responseData, ...aiResponse.responseData])
: aiResponse.responseData;
}
if (aiResponse.value) {
chatItem.value = chatItem.value ? [...chatItem.value, ...aiResponse.value] : aiResponse.value;
}
if (aiResponse.citeCollectionIds) {
chatItem.citeCollectionIds = chatItem.citeCollectionIds
? [...chatItem.citeCollectionIds, ...aiResponse.citeCollectionIds]
: aiResponse.citeCollectionIds;
}
chatItem.durationSeconds = chatItem.durationSeconds
? +(chatItem.durationSeconds + durationSeconds).toFixed(2)
@ -332,7 +365,7 @@ export const updateInteractiveChat = async ({
},
{
$set: {
variables: newVariables,
variables,
updateTime: new Date()
}
},
@ -340,5 +373,36 @@ export const updateInteractiveChat = async ({
session
}
);
// Create chat item respones
if (nodeResponses) {
// Merge
const lastResponse = await MongoChatItemResponse.findOneAndDelete({
appId,
chatId,
chatItemDataId: chatItem.dataId
})
.sort({
_id: -1
})
.lean()
.session(session);
const newResponses = lastResponse?.data
? // @ts-ignore
mergeChatResponseData([lastResponse?.data, ...nodeResponses])
: nodeResponses;
await MongoChatItemResponse.create(
newResponses.map((item) => ({
teamId,
appId,
chatId,
chatItemDataId: chatItem.dataId,
data: item
})),
{ session, ordered: true, ...writePrimary }
);
}
});
};

View File

@ -1,8 +1,4 @@
import {
DatasetCollectionTypeEnum,
DatasetCollectionDataProcessModeEnum,
DatasetTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { DatasetCollectionDataProcessModeEnum } from '@fastgpt/global/core/dataset/constants';
import type { CreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d';
import { MongoDatasetCollection } from './schema';
import type {
@ -25,9 +21,7 @@ import { createTrainingUsage } from '../../../support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { getLLMModel, getEmbeddingModel, getVlmModel } from '../../ai/model';
import { pushDataListToTrainingQueue, pushDatasetToParseQueue } from '../training/controller';
import { MongoImage } from '../../../common/file/image/schema';
import { hashStr } from '@fastgpt/global/common/string/tools';
import { addDays } from 'date-fns';
import { MongoDatasetDataText } from '../data/dataTextSchema';
import { retryFn } from '@fastgpt/global/common/system/utils';
import { getTrainingModeByCollection } from './utils';
@ -184,9 +178,9 @@ export const createCollectionAndInsertData = async ({
});
// 4. create training bill
const traingBillId = await (async () => {
const traingUsageId = await (async () => {
if (billId) return billId;
const { billId: newBillId } = await createTrainingUsage({
const { usageId: newUsageId } = await createTrainingUsage({
teamId,
tmbId,
appName: formatCreateCollectionParams.name,
@ -196,7 +190,7 @@ export const createCollectionAndInsertData = async ({
vllmModel: getVlmModel(dataset.vlmModel)?.name,
session
});
return newBillId;
return newUsageId;
})();
// 5. insert to training queue
@ -212,7 +206,7 @@ export const createCollectionAndInsertData = async ({
vlmModel: dataset.vlmModel,
indexSize,
mode: trainingMode,
billId: traingBillId,
billId: traingUsageId,
data: chunks.map((item, index) => ({
...item,
indexes: item.indexes?.map((text) => ({
@ -229,7 +223,7 @@ export const createCollectionAndInsertData = async ({
tmbId,
datasetId: dataset._id,
collectionId,
billId: traingBillId,
billId: traingUsageId,
session
});
return {

View File

@ -156,7 +156,8 @@ export const readDatasetSourceRawText = async ({
externalFileId,
apiDatasetServer,
customPdfParse,
getFormatText
getFormatText,
usageId
}: {
teamId: string;
tmbId: string;
@ -168,6 +169,7 @@ export const readDatasetSourceRawText = async ({
selector?: string; // link selector
externalFileId?: string; // external file dataset
apiDatasetServer?: ApiDatasetServerType; // api dataset
usageId?: string;
}): Promise<{
title?: string;
rawText: string;
@ -178,8 +180,9 @@ export const readDatasetSourceRawText = async ({
tmbId,
bucketName: BucketNameEnum.dataset,
fileId: sourceId,
getFormatText,
customPdfParse,
getFormatText
usageId
});
return {
title: filename,

View File

@ -63,6 +63,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
...props,
runningAppInfo: {
id: String(appData._id),
name: appData.name,
teamId: String(appData.teamId),
tmbId: String(appData.tmbId)
},

View File

@ -48,6 +48,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
lastInteractive,
runningUserInfo,
externalProvider,
usageId,
params: {
model,
systemPrompt,
@ -117,7 +118,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
customPdfParse: chatConfig?.fileSelectConfig?.customPdfParse,
fileLinks,
inputFiles: globalFiles,
hasReadFilesTool
hasReadFilesTool,
usageId
});
const concatenateSystemPrompt = [
@ -273,7 +275,8 @@ const getMultiInput = async ({
maxFiles,
customPdfParse,
inputFiles,
hasReadFilesTool
hasReadFilesTool,
usageId
}: {
runningUserInfo: ChatDispatchProps['runningUserInfo'];
histories: ChatItemType[];
@ -283,6 +286,7 @@ const getMultiInput = async ({
customPdfParse?: boolean;
inputFiles: UserChatItemValueItemType['file'][];
hasReadFilesTool: boolean;
usageId?: string;
}) => {
// Not file quote
if (!fileLinks || hasReadFilesTool) {
@ -309,6 +313,7 @@ const getMultiInput = async ({
requestOrigin,
maxFiles,
customPdfParse,
usageId,
teamId: runningUserInfo.teamId,
tmbId: runningUserInfo.tmbId
});

View File

@ -74,6 +74,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
runningUserInfo,
workflowStreamResponse,
chatConfig,
usageId,
params: {
model,
temperature,
@ -131,6 +132,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
requestOrigin,
maxFiles: chatConfig?.fileSelectConfig?.maxFiles || 20,
customPdfParse: chatConfig?.fileSelectConfig?.customPdfParse,
usageId,
runningUserInfo
})
]);
@ -313,6 +315,7 @@ async function getMultiInput({
requestOrigin,
maxFiles,
customPdfParse,
usageId,
runningUserInfo
}: {
histories: ChatItemType[];
@ -322,6 +325,7 @@ async function getMultiInput({
requestOrigin?: string;
maxFiles: number;
customPdfParse?: boolean;
usageId?: string;
runningUserInfo: ChatDispatchProps['runningUserInfo'];
}) {
// 旧版本适配====>
@ -359,9 +363,10 @@ async function getMultiInput({
urls,
requestOrigin,
maxFiles,
customPdfParse,
teamId: runningUserInfo.teamId,
tmbId: runningUserInfo.tmbId
tmbId: runningUserInfo.tmbId,
customPdfParse,
usageId
});
return {

View File

@ -20,7 +20,7 @@ import { authAppByTmbId } from '../../../../support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppVersionById } from '../../../app/version/controller';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { getUserChatInfoAndAuthTeamPoints } from '../../../../support/permission/auth/team';
import { getUserChatInfo } from '../../../../support/user/team/utils';
import { getRunningUserInfoByTmbId } from '../../../../support/user/team/utils';
type Props = ModuleDispatchProps<{
@ -99,7 +99,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
// Rewrite children app variables
const systemVariables = filterSystemVariables(variables);
const { externalProvider } = await getUserChatInfoAndAuthTeamPoints(appData.tmbId);
const { externalProvider } = await getUserChatInfo(appData.tmbId);
const childrenRunVariables = {
...systemVariables,
...childrenAppVariables,
@ -144,6 +144,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
: {}),
runningAppInfo: {
id: String(appData._id),
name: appData.name,
teamId: String(appData.teamId),
tmbId: String(appData.tmbId),
isChildApp: true

View File

@ -10,6 +10,7 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { MCPClient } from '../../../app/mcp';
import { getSecretValue } from '../../../../common/secret/utils';
import type { McpToolDataType } from '@fastgpt/global/core/app/mcpTools/type';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/type';
import { APIRunSystemTool } from '../../../app/tool/api';
import { MongoSystemPlugin } from '../../../app/plugin/systemPluginSchema';
import { SystemToolInputTypeEnum } from '@fastgpt/global/core/app/systemTool/constants';
@ -20,6 +21,7 @@ import { pushTrack } from '../../../../common/middle/tracks/utils';
import { getNodeErrResponse } from '../utils';
import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils';
import { getAppVersionById } from '../../../../core/app/version/controller';
import { runHTTPTool } from '../../../app/http';
type SystemInputConfigType = {
type: SystemToolInputTypeEnum;
@ -34,7 +36,7 @@ type RunToolProps = ModuleDispatchProps<{
type RunToolResponse = DispatchNodeResultType<
{
[NodeOutputKeyEnum.rawResponse]?: any; // MCP Tool
[NodeOutputKeyEnum.rawResponse]?: any;
[key: string]: any;
},
Record<string, any>
@ -214,6 +216,60 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
},
[DispatchNodeResponseKeyEnum.toolResponses]: result
};
} else if (toolConfig?.httpTool?.toolId) {
const { pluginId } = splitCombinePluginId(toolConfig.httpTool.toolId);
const [parentId, toolSetName, toolName] = pluginId.split('/');
const toolset = await getAppVersionById({
appId: parentId,
versionId: version
});
const toolSetData = toolset.nodes[0].toolConfig?.httpToolSet;
if (!toolSetData || typeof toolSetData !== 'object') {
throw new Error('HTTP tool set not found');
}
const { headerSecret, baseUrl, toolList, customHeaders } = toolSetData;
const httpTool = toolList?.find((tool: HttpToolConfigType) => tool.name === toolName);
if (!httpTool) {
throw new Error(`HTTP tool ${toolName} not found`);
}
const { data, errorMsg } = await runHTTPTool({
baseUrl: baseUrl,
toolPath: httpTool.path,
method: httpTool.method,
params,
headerSecret,
customHeaders: customHeaders
? typeof customHeaders === 'string'
? JSON.parse(customHeaders)
: customHeaders
: undefined
});
if (errorMsg) {
if (catchError) {
return {
error: { [NodeOutputKeyEnum.errorText]: errorMsg },
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolRes: errorMsg,
moduleLogo: avatar
},
[DispatchNodeResponseKeyEnum.toolResponses]: errorMsg
};
}
throw new Error(errorMsg);
}
return {
data: { [NodeOutputKeyEnum.rawResponse]: data, ...(typeof data === 'object' ? data : {}) },
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolRes: data,
moduleLogo: avatar
},
[DispatchNodeResponseKeyEnum.toolResponses]: data
};
} else {
// mcp tool (old version compatible)
const { toolData, system_toolData, ...restParams } = params;

View File

@ -15,7 +15,6 @@ import { type ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type
import { MongoDataset } from '../../../dataset/schema';
import { i18nT } from '../../../../../web/i18n/utils';
import { filterDatasetsByTmbId } from '../../../dataset/utils';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { getDatasetSearchToolResponsePrompt } from '../../../../../global/core/ai/prompt/dataset';
import { getNodeErrResponse } from '../utils';

View File

@ -47,8 +47,13 @@ import { rewriteRuntimeWorkFlow, removeSystemVariable } from './utils';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { callbackMap } from './constants';
import { anyValueDecrypt } from '../../../common/secret/utils';
import { getUserChatInfo } from '../../../support/user/team/utils';
import { checkTeamAIPoints } from '../../../support/permission/teamLimit';
import type { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { createChatUsageRecord, pushChatItemUsage } from '../../../support/wallet/usage/controller';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
type Props = Omit<ChatDispatchProps, 'workflowDispatchDeep'> & {
type Props = Omit<ChatDispatchProps, 'workflowDispatchDeep' | 'timezone' | 'externalProvider'> & {
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
defaultSkipNodeQueue?: WorkflowDebugResponse['skipNodeQueue'];
@ -61,8 +66,38 @@ type NodeResponseCompleteType = Omit<NodeResponseType, 'responseData'> & {
};
// Run workflow
export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowResponse> {
const { res, stream, externalProvider } = data;
type WorkflowUsageProps = RequireOnlyOne<{
usageSource: UsageSourceEnum;
concatUsage: (points: number) => any;
usageId: string;
}>;
export async function dispatchWorkFlow({
usageSource,
usageId,
concatUsage,
...data
}: Props & WorkflowUsageProps): Promise<DispatchFlowResponse> {
const { res, stream, runningUserInfo, runningAppInfo, lastInteractive } = data;
await checkTeamAIPoints(runningUserInfo.teamId);
const [{ timezone, externalProvider }, newUsageId] = await Promise.all([
getUserChatInfo(runningUserInfo.tmbId),
(() => {
if (lastInteractive?.usageId) {
return lastInteractive.usageId;
}
if (usageSource) {
return createChatUsageRecord({
appName: runningAppInfo.name,
appId: runningAppInfo.id,
teamId: runningUserInfo.teamId,
tmbId: runningUserInfo.tmbId,
source: usageSource
});
}
return usageId;
})()
]);
let streamCheckTimer: NodeJS.Timeout | null = null;
@ -96,15 +131,22 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
// Get default variables
const defaultVariables = {
...externalProvider.externalWorkflowVariables,
...getSystemVariables(data)
...getSystemVariables({
...data,
timezone
})
};
// Init some props
return runWorkflow({
...data,
timezone,
externalProvider,
defaultSkipNodeQueue: data.lastInteractive?.skipNodeQueue || data.defaultSkipNodeQueue,
variables: defaultVariables,
workflowDispatchDeep: 0
workflowDispatchDeep: 0,
usageId: newUsageId,
concatUsage
}).finally(() => {
if (streamCheckTimer) {
clearInterval(streamCheckTimer);
@ -116,6 +158,7 @@ type RunWorkflowProps = ChatDispatchProps & {
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
defaultSkipNodeQueue?: WorkflowDebugResponse['skipNodeQueue'];
concatUsage?: (points: number) => any;
};
export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowResponse> => {
let {
@ -129,7 +172,10 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
retainDatasetCite = true,
version = 'v1',
responseDetail = true,
responseAllData = true
responseAllData = true,
usageId,
concatUsage,
runningUserInfo: { teamId }
} = data;
// Over max depth
@ -487,7 +533,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
data: responseAllData
? formatResponseData
: filterPublicNodeResponseData({
flowResponses: [formatResponseData],
nodeRespones: [formatResponseData],
responseDetail
})[0]
});
@ -573,6 +619,17 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}
if (nodeDispatchUsages) {
if (usageId) {
pushChatItemUsage({
teamId,
usageId,
nodeUsages: nodeDispatchUsages
});
}
if (concatUsage) {
concatUsage(nodeDispatchUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0));
}
this.chatNodeUsages = this.chatNodeUsages.concat(nodeDispatchUsages);
}
@ -827,7 +884,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
...edge,
status: entryNodeIds.includes(edge.target) ? 'active' : edge.status
})),
nodeOutputs
nodeOutputs,
usageId
};
// Tool call, not need interactive response
@ -945,7 +1003,9 @@ const getSystemVariables = ({
uid,
chatConfig,
variables
}: Props): SystemVariablesType => {
}: Props & {
timezone: string;
}): SystemVariablesType => {
// Get global variables(Label -> key; Key -> key)
const variablesConfig = chatConfig?.variables || [];

View File

@ -21,7 +21,7 @@ import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getChildAppRuntimeById } from '../../../app/plugin/controller';
import { runWorkflow } from '../index';
import { getUserChatInfoAndAuthTeamPoints } from '../../../../support/permission/auth/team';
import { getUserChatInfo } from '../../../../support/user/team/utils';
import { dispatchRunTool } from '../child/runTool';
import type { PluginRuntimeType } from '@fastgpt/global/core/app/plugin/type';
@ -111,7 +111,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
};
});
const { externalProvider } = await getUserChatInfoAndAuthTeamPoints(runningAppInfo.tmbId);
const { externalProvider } = await getUserChatInfo(runningAppInfo.tmbId);
const runtimeVariables = {
...filterSystemVariables(props.variables),
appId: String(plugin.id),
@ -129,6 +129,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
: {}),
runningAppInfo: {
id: String(plugin.id),
name: plugin.name,
// 如果系统插件有 teamId 和 tmbId则使用系统插件的 teamId 和 tmbId管理员指定了插件作为系统插件
teamId: plugin.teamId || runningAppInfo.teamId,
tmbId: plugin.tmbId || runningAppInfo.tmbId,

View File

@ -52,7 +52,8 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
histories,
chatConfig,
node: { version },
params: { fileUrlList = [] }
params: { fileUrlList = [] },
usageId
} = props;
const maxFiles = chatConfig?.fileSelectConfig?.maxFiles || 20;
const customPdfParse = chatConfig?.fileSelectConfig?.customPdfParse || false;
@ -68,7 +69,8 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
maxFiles,
teamId,
tmbId,
customPdfParse
customPdfParse,
usageId
});
return {
@ -119,7 +121,8 @@ export const getFileContentFromLinks = async ({
maxFiles,
teamId,
tmbId,
customPdfParse
customPdfParse,
usageId
}: {
urls: string[];
requestOrigin?: string;
@ -127,6 +130,7 @@ export const getFileContentFromLinks = async ({
teamId: string;
tmbId: string;
customPdfParse?: boolean;
usageId?: string;
}) => {
const parseUrlList = urls
// Remove invalid urls
@ -225,7 +229,8 @@ export const getFileContentFromLinks = async ({
buffer,
encoding,
customPdfParse,
getFormatText: true
getFormatText: true,
usageId
});
// Add to buffer

View File

@ -18,11 +18,13 @@ import {
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils';
import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/httpTools/utils';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { MongoApp } from '../../../core/app/schema';
import { getMCPChildren } from '../../../core/app/mcp';
import { getSystemToolRunTimeNodeFromSystemToolset } from '../utils';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/type';
export const getWorkflowResponseWrite = ({
res,
@ -197,7 +199,8 @@ export const rewriteRuntimeWorkFlow = async ({
for (const toolSetNode of toolSetNodes) {
nodeIdsToRemove.add(toolSetNode.nodeId);
const systemToolId = toolSetNode.toolConfig?.systemToolSet?.toolId;
const mcpToolsetVal = toolSetNode.toolConfig?.mcpToolSet ?? toolSetNode.inputs[0].value;
const mcpToolsetVal = toolSetNode.toolConfig?.mcpToolSet ?? toolSetNode.inputs?.[0]?.value;
const httpToolsetVal = toolSetNode.toolConfig?.httpToolSet;
const incomingEdges = edges.filter((edge) => edge.target === toolSetNode.nodeId);
const pushEdges = (nodeId: string) => {
@ -243,6 +246,22 @@ export const rewriteRuntimeWorkFlow = async ({
});
pushEdges(newToolNode.nodeId);
});
} else if (httpToolsetVal) {
const parentId = toolSetNode.pluginId || '';
httpToolsetVal.toolList.forEach((tool: HttpToolConfigType, index: number) => {
const newToolNode = getHTTPToolRuntimeNode({
tool: {
...tool,
name: `${toolSetNode.name}/${tool.name}`
},
nodeId: `${parentId}${index}`,
avatar: toolSetNode.avatar,
parentId
});
nodes.push(newToolNode);
pushEdges(newToolNode.nodeId);
});
}
}

View File

@ -27,6 +27,8 @@
"encoding": "^0.1.13",
"file-type": "^19.0.0",
"form-data": "^4.0.4",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"iconv-lite": "^0.6.3",
"ioredis": "^5.6.0",
"joplin-turndown-plugin-gfm": "^1.0.12",
@ -35,6 +37,7 @@
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"mammoth": "^1.6.0",
"minio": "^8.0.5",
"mongoose": "^8.10.1",
"multer": "2.0.2",
"mysql2": "^3.11.3",

View File

@ -1,32 +0,0 @@
import { MongoTeamMember } from '../../user/team/teamMemberSchema';
import { checkTeamAIPoints } from '../teamLimit';
import { type UserModelSchema } from '@fastgpt/global/support/user/type';
import { type TeamSchema } from '@fastgpt/global/support/user/team/type';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
export async function getUserChatInfoAndAuthTeamPoints(tmbId: string) {
const tmb = await MongoTeamMember.findById(tmbId, 'userId teamId')
.populate<{ user: UserModelSchema; team: TeamSchema }>([
{
path: 'user',
select: 'timezone'
},
{
path: 'team',
select: 'openaiAccount externalWorkflowVariables'
}
])
.lean();
if (!tmb) return Promise.reject(TeamErrEnum.notUser);
await checkTeamAIPoints(tmb.team._id);
return {
timezone: tmb.user.timezone,
externalProvider: {
openaiAccount: tmb.team.openaiAccount,
externalWorkflowVariables: tmb.team.externalWorkflowVariables
}
};
}

View File

@ -0,0 +1,46 @@
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { getGroupsByTmbId } from '../memberGroup/controllers';
import { getOrgsByTmbId } from '../org/controllers';
import { MongoResourcePermission } from '../schema';
import { getCollaboratorId } from '@fastgpt/global/support/permission/utils';
import { isProVersion } from '../../../common/system/constants';
export const getMyModels = async ({
teamId,
tmbId,
isTeamOwner
}: {
teamId: string;
tmbId: string;
isTeamOwner: boolean;
}) => {
if (isTeamOwner || !isProVersion()) {
return global.systemModelList.map((m) => m.model);
}
const [groups, orgs] = await Promise.all([
getGroupsByTmbId({
teamId,
tmbId
}),
getOrgsByTmbId({
teamId,
tmbId
})
]);
const myIdSet = new Set([tmbId, ...groups.map((g) => g._id), ...orgs.map((o) => o._id)]);
const rps = await MongoResourcePermission.find({
teamId,
resourceType: PerResourceTypeEnum.model
}).lean();
const permissionConfiguredModelSet = new Set(rps.map((rp) => rp.resourceName));
const unconfiguredModels = global.systemModelList.filter(
(model) => !permissionConfiguredModelSet.has(model.model)
);
const myModels = rps.filter((rp) => myIdSet.has(getCollaboratorId(rp)));
return [...unconfiguredModels.map((m) => m.model), ...myModels.map((m) => m.resourceName)];
};

View File

@ -41,14 +41,20 @@ export const ResourcePermissionSchema = new Schema({
type: Number,
required: true
},
/**
* Optional. Only be set when the resource is *inherited* from the parent resource.
* For recording the self permission. When cancel the inheritance, it will overwrite the permission property and set to `unset`.
*/
// Resource ID: App or DataSet or any other resource type.
// It is null if the resourceType is team.
resourceId: {
type: Schema.Types.ObjectId
},
/**
* Optional, For some resources, which do not have resourceId, the resourceName is required.
*/
resourceName: {
type: String
}
});
@ -72,6 +78,7 @@ ResourcePermissionSchema.virtual('org', {
});
try {
// Indexes for resourceId-based resources
ResourcePermissionSchema.index(
{
resourceType: 1,
@ -84,6 +91,9 @@ try {
partialFilterExpression: {
groupId: {
$exists: true
},
resourceId: {
$exists: true
}
}
}
@ -101,6 +111,9 @@ try {
partialFilterExpression: {
orgId: {
$exists: true
},
resourceId: {
$exists: true
}
}
}
@ -118,17 +131,106 @@ try {
partialFilterExpression: {
tmbId: {
$exists: true
},
resourceId: {
$exists: true
}
}
}
);
// Delete tmb permission
ResourcePermissionSchema.index({
resourceType: 1,
teamId: 1,
resourceId: 1
});
// General index for resourceId-based resources
ResourcePermissionSchema.index(
{
resourceType: 1,
teamId: 1,
resourceId: 1
},
{
partialFilterExpression: {
resourceId: {
$exists: true
}
}
}
);
// Indexes for resourceName-based resources
ResourcePermissionSchema.index(
{
resourceType: 1,
teamId: 1,
resourceName: 1,
groupId: 1
},
{
unique: true,
partialFilterExpression: {
groupId: {
$exists: true
},
resourceName: {
$exists: true
}
}
}
);
ResourcePermissionSchema.index(
{
resourceType: 1,
teamId: 1,
resourceName: 1,
orgId: 1
},
{
unique: true,
partialFilterExpression: {
orgId: {
$exists: true
},
resourceName: {
$exists: true
}
}
}
);
ResourcePermissionSchema.index(
{
resourceType: 1,
teamId: 1,
resourceName: 1,
tmbId: 1
},
{
unique: true,
partialFilterExpression: {
tmbId: {
$exists: true
},
resourceName: {
$exists: true
}
}
}
);
// General index for resourceName-based resources
ResourcePermissionSchema.index(
{
resourceType: 1,
teamId: 1,
resourceName: 1
},
{
partialFilterExpression: {
resourceName: {
$exists: true
}
}
}
);
} catch (error) {
console.log(error);
}

View File

@ -46,7 +46,13 @@ export const checkTeamAppLimit = async (teamId: string, amount = 1) => {
MongoApp.countDocuments({
teamId,
type: {
$in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin, AppTypeEnum.toolSet]
$in: [
AppTypeEnum.simple,
AppTypeEnum.workflow,
AppTypeEnum.plugin,
AppTypeEnum.toolSet,
AppTypeEnum.httpToolSet
]
}
})
]);

View File

@ -16,6 +16,7 @@ export function getI18nAppType(type: AppTypeEnum): string {
if (type === AppTypeEnum.workflow) return i18nT('account_team:type.Workflow bot');
if (type === AppTypeEnum.plugin) return i18nT('account_team:type.Plugin');
if (type === AppTypeEnum.httpPlugin) return i18nT('account_team:type.Http plugin');
if (type === AppTypeEnum.httpToolSet) return i18nT('account_team:type.Http tool set');
if (type === AppTypeEnum.toolSet) return i18nT('account_team:type.Tool set');
if (type === AppTypeEnum.tool) return i18nT('account_team:type.Tool');
return i18nT('common:UnKnow');

View File

@ -33,3 +33,28 @@ export async function getRunningUserInfoByTmbId(tmbId: string) {
return Promise.reject(TeamErrEnum.notUser);
}
export async function getUserChatInfo(tmbId: string) {
const tmb = await MongoTeamMember.findById(tmbId, 'userId teamId')
.populate<{ user: UserModelSchema; team: TeamSchema }>([
{
path: 'user',
select: 'timezone'
},
{
path: 'team',
select: 'openaiAccount externalWorkflowVariables'
}
])
.lean();
if (!tmb) return Promise.reject(TeamErrEnum.notUser);
return {
timezone: tmb.user.timezone,
externalProvider: {
openaiAccount: tmb.team.openaiAccount,
externalWorkflowVariables: tmb.team.externalWorkflowVariables
}
};
}

View File

@ -0,0 +1,2 @@
export const UsageCollectionName = 'usages';
export const UsageItemCollectionName = 'usage_items';

View File

@ -1,19 +1,21 @@
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { UsageItemTypeEnum, UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { MongoUsage } from './schema';
import { type ClientSession } from '../../../common/mongo';
import { addLog } from '../../../common/system/log';
import { type ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import {
type ConcatUsageProps,
type CreateUsageProps
import type {
PushUsageItemsProps,
ConcatUsageProps,
CreateUsageProps
} from '@fastgpt/global/support/wallet/usage/api';
import { i18nT } from '../../../../web/i18n/utils';
import { formatModelChars2Points } from './utils';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
import { MongoUsageItem } from './usageItemSchema';
export async function createUsage(data: CreateUsageProps) {
try {
await global.createUsageHandler(data);
return await global.createUsageHandler(data);
} catch (error) {
addLog.error('createUsage error', error);
}
@ -25,15 +27,94 @@ export async function concatUsage(data: ConcatUsageProps) {
addLog.error('concatUsage error', error);
}
}
export async function pushUsageItems(data: PushUsageItemsProps) {
try {
await global.pushUsageItemsHandler(data);
} catch (error) {
addLog.error('pushUsageItems error', error);
}
}
export const createChatUsage = ({
export const createPdfParseUsage = async ({
teamId,
tmbId,
pages,
usageId
}: {
teamId: string;
tmbId: string;
pages: number;
usageId?: string;
}) => {
const unitPrice = global.systemEnv?.customPdfParse?.price || 0;
const totalPoints = pages * unitPrice;
if (usageId) {
pushUsageItems({
teamId,
usageId,
list: [{ moduleName: i18nT('account_usage:pdf_enhanced_parse'), amount: totalPoints, pages }]
});
} else {
createUsage({
teamId,
tmbId,
appName: i18nT('account_usage:pdf_enhanced_parse'),
totalPoints,
source: UsageSourceEnum.pdfParse,
list: [
{
moduleName: i18nT('account_usage:pdf_enhanced_parse'),
amount: totalPoints,
pages
}
]
});
}
};
export const pushLLMTrainingUsage = async ({
teamId,
model,
inputTokens,
outputTokens,
usageId,
type
}: {
teamId: string;
model: string;
inputTokens: number;
outputTokens: number;
usageId: string;
type: UsageItemTypeEnum;
}) => {
// Compute points
const { totalPoints } = formatModelChars2Points({
model,
inputTokens,
outputTokens
});
concatUsage({
usageId,
teamId,
itemType: type,
totalPoints,
inputTokens,
outputTokens
});
return { totalPoints };
};
/* Create usage, and return usageId */
// Chat
export const createChatUsageRecord = async ({
appName,
appId,
pluginId,
teamId,
tmbId,
source,
flowUsages
source
}: {
appName: string;
appId?: string;
@ -41,42 +122,46 @@ export const createChatUsage = ({
teamId: string;
tmbId: string;
source: UsageSourceEnum;
flowUsages: ChatNodeUsageType[];
}) => {
const totalPoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0);
createUsage({
const [{ _id: usageId }] = await MongoUsage.create(
[
{
teamId,
tmbId,
appId,
pluginId,
appName,
source,
totalPoints: 0
}
],
{ ordered: true }
);
return String(usageId);
};
export const pushChatItemUsage = ({
teamId,
usageId,
nodeUsages
}: {
teamId: string;
usageId: string;
nodeUsages: ChatNodeUsageType[];
}) => {
pushUsageItems({
teamId,
tmbId,
appName,
appId,
pluginId,
totalPoints,
source,
list: flowUsages.map((item) => ({
usageId,
list: nodeUsages.map((item) => ({
moduleName: item.moduleName,
amount: item.totalPoints || 0,
amount: item.totalPoints,
model: item.model,
inputTokens: item.inputTokens,
outputTokens: item.outputTokens
}))
});
addLog.debug(`Create chat usage`, {
source,
teamId,
totalPoints
});
return { totalPoints };
};
export type DatasetTrainingMode = 'paragraph' | 'qa' | 'autoIndex' | 'imageIndex' | 'imageParse';
export const datasetTrainingUsageIndexMap: Record<DatasetTrainingMode, number> = {
paragraph: 1,
qa: 2,
autoIndex: 3,
imageIndex: 4,
imageParse: 5
};
// Dataset training
export const createTrainingUsage = async ({
teamId,
tmbId,
@ -91,189 +176,161 @@ export const createTrainingUsage = async ({
tmbId: string;
appName: string;
billSource: UsageSourceEnum;
vectorModel?: string;
vectorModel: string;
agentModel?: string;
vllmModel?: string;
session?: ClientSession;
}) => {
const [{ _id }] = await MongoUsage.create(
[
const create = async (session: ClientSession) => {
const [result] = await MongoUsage.create(
[
{
teamId,
tmbId,
source: billSource,
appName,
totalPoints: 0
}
],
{ session, ordered: true }
);
await MongoUsageItem.create(
[
{
teamId,
usageId: result._id,
itemType: UsageItemTypeEnum.training_vector,
name: i18nT('account_usage:embedding_index'),
model: vectorModel,
amount: 0,
inputTokens: 0
},
...(agentModel
? [
{
teamId,
usageId: result._id,
itemType: UsageItemTypeEnum.training_paragraph,
name: i18nT('account_usage:llm_paragraph'),
model: agentModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
teamId,
usageId: result._id,
itemType: UsageItemTypeEnum.training_qa,
name: i18nT('account_usage:qa'),
model: agentModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
teamId,
usageId: result._id,
itemType: UsageItemTypeEnum.training_autoIndex,
name: i18nT('account_usage:auto_index'),
model: agentModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
}
]
: []),
...(vllmModel
? [
{
teamId,
usageId: result._id,
itemType: UsageItemTypeEnum.training_imageIndex,
name: i18nT('account_usage:image_index'),
model: vllmModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
teamId,
usageId: result._id,
itemType: UsageItemTypeEnum.training_imageParse,
name: i18nT('account_usage:image_parse'),
model: vllmModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
}
]
: [])
],
{
teamId,
tmbId,
appName,
source: billSource,
totalPoints: 0,
list: [
...(vectorModel
? [
{
moduleName: i18nT('account_usage:embedding_index'),
model: vectorModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
}
]
: []),
...(agentModel
? [
{
moduleName: i18nT('account_usage:llm_paragraph'),
model: agentModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
moduleName: i18nT('account_usage:qa'),
model: agentModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
moduleName: i18nT('account_usage:auto_index'),
model: agentModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
}
]
: []),
...(vllmModel
? [
{
moduleName: i18nT('account_usage:image_index'),
model: vllmModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
moduleName: i18nT('account_usage:image_parse'),
model: vllmModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
}
]
: [])
]
session,
ordered: true
}
],
{ session, ordered: true }
);
);
return { billId: String(_id) };
};
export const createPdfParseUsage = async ({
teamId,
tmbId,
pages
}: {
teamId: string;
tmbId: string;
pages: number;
}) => {
const unitPrice = global.systemEnv?.customPdfParse?.price || 0;
const totalPoints = pages * unitPrice;
createUsage({
teamId,
tmbId,
appName: i18nT('account_usage:pdf_enhanced_parse'),
totalPoints,
source: UsageSourceEnum.pdfParse,
list: [
{
moduleName: i18nT('account_usage:pdf_enhanced_parse'),
amount: totalPoints,
pages
}
]
});
};
export const pushLLMTrainingUsage = async ({
teamId,
tmbId,
model,
inputTokens,
outputTokens,
billId,
mode
}: {
teamId: string;
tmbId: string;
model: string;
inputTokens: number;
outputTokens: number;
billId: string;
mode: DatasetTrainingMode;
}) => {
const index = datasetTrainingUsageIndexMap[mode];
// Compute points
const { totalPoints } = formatModelChars2Points({
model,
inputTokens,
outputTokens
});
concatUsage({
billId,
teamId,
tmbId,
totalPoints,
inputTokens,
outputTokens,
listIndex: index
});
return { totalPoints };
return { usageId: String(result._id) };
};
if (session) return create(session);
return mongoSessionRun(create);
};
// Evaluation
export const createEvaluationUsage = async ({
teamId,
tmbId,
appName,
model,
session
model
}: {
teamId: string;
tmbId: string;
appName: string;
model: string;
session?: ClientSession;
}) => {
const [{ _id: usageId }] = await MongoUsage.create(
[
const { usageId } = await mongoSessionRun(async (session) => {
const [{ _id: usageId }] = await MongoUsage.create(
[
{
teamId,
tmbId,
appName,
source: UsageSourceEnum.evaluation,
totalPoints: 0
}
],
{ session, ordered: true }
);
await MongoUsageItem.create(
[
{
teamId,
usageId,
itemType: UsageItemTypeEnum.evaluation_generateAnswer,
name: i18nT('account_usage:generate_answer'),
amount: 0,
count: 0
},
{
teamId,
usageId,
itemType: UsageItemTypeEnum.evaluation_answerAccuracy,
name: i18nT('account_usage:answer_accuracy'),
amount: 0,
inputTokens: 0,
outputTokens: 0,
model
}
],
{
teamId,
tmbId,
appName,
source: UsageSourceEnum.evaluation,
totalPoints: 0,
list: [
{
moduleName: i18nT('account_usage:generate_answer'),
amount: 0,
count: 0
},
{
moduleName: i18nT('account_usage:answer_accuracy'),
amount: 0,
inputTokens: 0,
outputTokens: 0,
model
}
]
session,
ordered: true
}
],
{ session, ordered: true }
);
);
return { usageId: String(usageId) };
});
return { usageId };
};

View File

@ -1,4 +1,4 @@
import { connectionMongo, getMongoModel, type Model } from '../../../common/mongo';
import { connectionMongo, getMongoModel } from '../../../common/mongo';
const { Schema } = connectionMongo;
import { type UsageSchemaType } from '@fastgpt/global/support/wallet/usage/type';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
@ -6,8 +6,7 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
export const UsageCollectionName = 'usages';
import { UsageCollectionName, UsageItemCollectionName } from './constants';
const UsageSchema = new Schema({
teamId: {
@ -30,6 +29,11 @@ const UsageSchema = new Schema({
type: String,
default: ''
},
totalPoints: {
// total points
type: Number,
required: true
},
appId: {
type: Schema.Types.ObjectId,
ref: 'apps',
@ -44,26 +48,21 @@ const UsageSchema = new Schema({
type: Date,
default: () => new Date()
},
totalPoints: {
// total points
type: Number,
required: true
},
// total: {
// // total points
// type: Number,
// required: true
// },
// @description It will not be used again in the future.
list: {
type: Array,
default: []
type: Array
}
});
UsageSchema.virtual('usageItems', {
ref: UsageItemCollectionName,
localField: '_id',
foreignField: 'usageId'
});
try {
UsageSchema.index({ teamId: 1, tmbId: 1, source: 1, time: 1, appName: 1, _id: -1 });
// timer task. clear dead team
// UsageSchema.index({ teamId: 1, time: -1 });
UsageSchema.index({ time: 1 }, { expireAfterSeconds: 360 * 24 * 60 * 60 });
} catch (error) {

View File

@ -1,16 +1,6 @@
export type ConcatBillQueueItemType = {
billId: string; // usageId
listIndex?: number;
totalPoints: number;
// Model usage
inputTokens?: number;
outputTokens?: number;
// Times
count?: number;
};
import type { ConcatUsageProps } from '@fastgpt/global/support/wallet/usage/api';
declare global {
var reduceAiPointsQueue: { teamId: string; totalPoints: number }[];
var concatBillQueue: ConcatBillQueueItemType[];
var concatBillQueue: ConcatUsageProps[];
}

View File

@ -0,0 +1,53 @@
import { connectionMongo, getMongoModel } from '../../../common/mongo';
const { Schema } = connectionMongo;
import type { UsageItemSchemaType } from '@fastgpt/global/support/wallet/usage/type';
import { UsageCollectionName, UsageItemCollectionName } from './constants';
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
const UsageItemSchema = new Schema({
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
usageId: {
type: Schema.Types.ObjectId,
ref: UsageCollectionName,
required: true
},
name: {
// usage name
type: String,
required: true
},
amount: {
type: Number,
default: 0
},
itemType: Number,
time: {
type: Date,
default: () => new Date()
},
// Params
inputTokens: Number,
outputTokens: Number,
charsLength: Number,
duration: Number,
pages: Number,
count: Number,
model: String
});
try {
UsageItemSchema.index({ usageId: 'hashed' });
UsageItemSchema.index({ time: 1 }, { expireAfterSeconds: 360 * 24 * 60 * 60 });
} catch (error) {
console.log(error);
}
export const MongoUsageItem = getMongoModel<UsageItemSchemaType>(
UsageItemCollectionName,
UsageItemSchema
);

View File

@ -1,9 +1,9 @@
import { createClient } from '@fastgpt/global/sdk/fastgpt-plugin';
export const BASE_URL = process.env.PLUGIN_BASE_URL || '';
export const TOKEN = process.env.PLUGIN_TOKEN || '';
export const PLUGIN_BASE_URL = process.env.PLUGIN_BASE_URL || '';
export const PLUGIN_TOKEN = process.env.PLUGIN_TOKEN || '';
export const pluginClient = createClient({
baseUrl: BASE_URL,
token: TOKEN
baseUrl: PLUGIN_BASE_URL,
token: PLUGIN_TOKEN
});

View File

@ -43,9 +43,18 @@ const parsePowerPoint = async ({
return Promise.reject('解析 PPT 失败');
}
// Sort files by slide number to ensure correct order
const sortedFiles = files.sort((a, b) => {
const getSlideNumber = (path: string) => {
const match = path.match(/\d+/);
return match ? parseInt(match[0]) : 0;
};
return getSlideNumber(a.path) - getSlideNumber(b.path);
});
// Returning an array of all the xml contents read using fs.readFileSync
const xmlContentArray = await Promise.all(
files.map(async (file) => {
sortedFiles.map(async (file) => {
try {
return await fs.promises.readFile(`${decompressPath}/${file.path}`, encoding);
} catch (err) {

View File

@ -1,6 +1,6 @@
import React, { useState, useMemo, useRef, useEffect } from 'react';
import type { BoxProps } from '@chakra-ui/react';
import { Box, Card, Flex, useOutsideClick, Button } from '@chakra-ui/react';
import { Box, Card, Flex, useTheme, useOutsideClick, Button } from '@chakra-ui/react';
import { addDays, format } from 'date-fns';
import { DayPicker } from 'react-day-picker';
import 'react-day-picker/dist/style.css';

View File

@ -13,6 +13,7 @@ export const iconPaths = {
close: () => import('./icons/close.svg'),
closeSolid: () => import('./icons/closeSolid.svg'),
code: () => import('./icons/code.svg'),
codeCopilot: () => import('./icons/codeCopilot.svg'),
collectionLight: () => import('./icons/collectionLight.svg'),
collectionSolid: () => import('./icons/collectionSolid.svg'),
comment: () => import('./icons/comment.svg'),

View File

@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
<path d="M4.65137 13.7275C4.98754 13.7275 5.26074 14.0128 5.26074 14.3643C5.2606 14.7156 4.98745 15 4.65137 15C4.31537 14.9999 4.04311 14.7155 4.04297 14.3643C4.04297 14.0129 4.31528 13.7276 4.65137 13.7275Z" fill="url(#paint0_linear_26054_96715)"/>
<path d="M2.01855 11.3457C2.06906 11.1279 2.36551 11.1279 2.41602 11.3457L2.55469 11.9443C2.57269 12.0219 2.63092 12.0826 2.70508 12.1016L3.27832 12.2471C3.48661 12.2999 3.48661 12.6093 3.27832 12.6621L2.70508 12.8076C2.63091 12.8265 2.57273 12.8873 2.55469 12.9648L2.41602 13.5645C2.36538 13.782 2.06915 13.782 2.01855 13.5645L1.87988 12.9648C1.86181 12.8872 1.80374 12.8265 1.72949 12.8076L1.15625 12.6621C0.948018 12.6093 0.94798 12.2999 1.15625 12.2471L1.72949 12.1016C1.80375 12.0826 1.86187 12.022 1.87988 11.9443L2.01855 11.3457Z" fill="url(#paint1_linear_26054_96715)"/>
<path d="M8.82227 3.50586C9.19608 3.61202 9.41664 4.00027 9.31445 4.37402C9.02199 5.44297 8.64314 7.12446 8.33301 8.57617C8.17854 9.29922 8.04241 9.96146 7.94336 10.4541C7.89386 10.7003 7.85325 10.9038 7.8252 11.0508C7.7996 11.1849 7.78871 11.2467 7.78613 11.2598C7.74665 11.6461 7.40153 11.9249 7.01562 11.8818C6.62991 11.8387 6.34941 11.4906 6.38867 11.1045C6.39858 11.0085 6.47241 10.6316 6.56641 10.1641C6.66658 9.66585 6.80339 8.99884 6.95898 8.27051C7.26915 6.81866 7.65498 5.10146 7.95898 3.99023C8.06148 3.61653 8.44823 3.39994 8.82227 3.50586Z" fill="url(#paint2_linear_26054_96715)"/>
<path d="M4.34766 4.2959C4.63622 4.03649 5.08015 4.06029 5.33984 4.34863C5.59948 4.63715 5.57641 5.08103 5.28809 5.34082L2.68555 7.68262L5.28809 10.0234C5.57655 10.2832 5.59955 10.728 5.33984 11.0166C5.08006 11.3046 4.63608 11.3278 4.34766 11.0684L1.41309 8.42773L1.33496 8.34961C0.996541 7.97076 0.996493 7.39347 1.33496 7.01465L1.41309 6.93652L4.34766 4.2959Z" fill="url(#paint3_linear_26054_96715)"/>
<path d="M10.6592 4.34863C10.9188 4.06017 11.3627 4.03659 11.6514 4.2959L14.5859 6.93652L14.6641 7.01465C15.0028 7.39353 15.0027 7.97067 14.6641 8.34961L14.5859 8.42773L11.6514 11.0684C11.3629 11.3276 10.9189 11.3048 10.6592 11.0166C10.3997 10.7281 10.4227 10.2832 10.7109 10.0234L13.3135 7.68262L10.7109 5.34082C10.4228 5.08107 10.3998 4.6371 10.6592 4.34863Z" fill="url(#paint4_linear_26054_96715)"/>
<path d="M13.584 2.43652C13.6345 2.21877 13.93 2.21877 13.9805 2.43652L14.1201 3.03516C14.1381 3.1129 14.1961 3.17451 14.2705 3.19336L14.8438 3.33789C15.0518 3.39083 15.0518 3.701 14.8438 3.75391L14.2705 3.89844C14.1963 3.91726 14.1383 3.97812 14.1201 4.05566L13.9805 4.65527C13.9299 4.87285 13.6346 4.87287 13.584 4.65527L13.4443 4.05566C13.4262 3.97814 13.3682 3.91725 13.2939 3.89844L12.7207 3.75391C12.5128 3.70092 12.5128 3.39091 12.7207 3.33789L13.2939 3.19336C13.3683 3.17451 13.4263 3.1129 13.4443 3.03516L13.584 2.43652Z" fill="url(#paint5_linear_26054_96715)"/>
<path d="M11.3477 1C11.6838 1 11.957 1.28526 11.957 1.63672C11.957 1.98816 11.6838 2.27344 11.3477 2.27344C11.0116 2.27326 10.7393 1.98806 10.7393 1.63672C10.7393 1.28537 11.0116 1.00017 11.3477 1Z" fill="url(#paint6_linear_26054_96715)"/>
<defs>
<linearGradient id="paint0_linear_26054_96715" x1="7.99986" y1="1.45455" x2="7.99986" y2="14.5455" gradientUnits="userSpaceOnUse">
<stop stop-color="#499DFF"/>
<stop offset="0.432292" stop-color="#2770FF"/>
<stop offset="1" stop-color="#6E80FF"/>
</linearGradient>
<linearGradient id="paint1_linear_26054_96715" x1="7.99986" y1="1.45455" x2="7.99986" y2="14.5455" gradientUnits="userSpaceOnUse">
<stop stop-color="#499DFF"/>
<stop offset="0.432292" stop-color="#2770FF"/>
<stop offset="1" stop-color="#6E80FF"/>
</linearGradient>
<linearGradient id="paint2_linear_26054_96715" x1="7.99986" y1="1.45455" x2="7.99986" y2="14.5455" gradientUnits="userSpaceOnUse">
<stop stop-color="#499DFF"/>
<stop offset="0.432292" stop-color="#2770FF"/>
<stop offset="1" stop-color="#6E80FF"/>
</linearGradient>
<linearGradient id="paint3_linear_26054_96715" x1="7.99986" y1="1.45455" x2="7.99986" y2="14.5455" gradientUnits="userSpaceOnUse">
<stop stop-color="#499DFF"/>
<stop offset="0.432292" stop-color="#2770FF"/>
<stop offset="1" stop-color="#6E80FF"/>
</linearGradient>
<linearGradient id="paint4_linear_26054_96715" x1="7.99986" y1="1.45455" x2="7.99986" y2="14.5455" gradientUnits="userSpaceOnUse">
<stop stop-color="#499DFF"/>
<stop offset="0.432292" stop-color="#2770FF"/>
<stop offset="1" stop-color="#6E80FF"/>
</linearGradient>
<linearGradient id="paint5_linear_26054_96715" x1="7.99986" y1="1.45455" x2="7.99986" y2="14.5455" gradientUnits="userSpaceOnUse">
<stop stop-color="#499DFF"/>
<stop offset="0.432292" stop-color="#2770FF"/>
<stop offset="1" stop-color="#6E80FF"/>
</linearGradient>
<linearGradient id="paint6_linear_26054_96715" x1="7.99986" y1="1.45455" x2="7.99986" y2="14.5455" gradientUnits="userSpaceOnUse">
<stop stop-color="#499DFF"/>
<stop offset="0.432292" stop-color="#2770FF"/>
<stop offset="1" stop-color="#6E80FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -225,7 +225,7 @@ const MyMenu = ({
autoSelect={false}
direction={'ltr'}
isLazy
lazyBehavior={'keepMounted'}
lazyBehavior={'unmount'}
placement={placement}
computePositionOnMount
>

View File

@ -73,7 +73,7 @@ const PopoverConfirm = ({
openDelay={100}
closeDelay={100}
isLazy
lazyBehavior="keepMounted"
lazyBehavior="unmount"
arrowSize={10}
strategy={'fixed'}
computePositionOnMount={true}

View File

@ -60,7 +60,7 @@ const MyPopover = ({
openDelay={100}
closeDelay={100}
isLazy
lazyBehavior="keepMounted"
lazyBehavior="unmount"
autoFocus={false}
>
<PopoverTrigger>{Trigger}</PopoverTrigger>

View File

@ -1,7 +1,6 @@
import type { FlexProps } from '@chakra-ui/react';
import {
Box,
Button,
type ButtonProps,
Checkbox,
Flex,

View File

@ -6,6 +6,7 @@
*
*/
import type { CSSProperties } from 'react';
import { useMemo, useState, useTransition } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
@ -40,18 +41,17 @@ import { useDeepCompareEffect } from 'ahooks';
import VariablePickerPlugin from './plugins/VariablePickerPlugin';
import MarkdownPlugin from './plugins/MarkdownPlugin';
import MyIcon from '../../Icon';
import TabToSpacesPlugin from './plugins/TabToSpacesPlugin';
import ListExitPlugin from './plugins/ListExitPlugin';
import KeyDownPlugin from './plugins/KeyDownPlugin';
const Placeholder = ({ children }: { children: React.ReactNode }) => (
const Placeholder = ({ children, padding }: { children: React.ReactNode; padding: string }) => (
<Box
position={'absolute'}
top={0}
left={0}
right={0}
bottom={0}
py={3}
px={3.5}
p={padding}
pointerEvents={'none'}
overflow={'hidden'}
>
@ -78,12 +78,14 @@ export type EditorProps = {
maxH?: number;
maxLength?: number;
placeholder?: string;
placeholderPadding?: string;
isInvalid?: boolean;
onKeyDown?: (e: React.KeyboardEvent) => void;
ExtensionPopover?: ((e: {
onChangeText: (text: string) => void;
iconButtonStyle: Record<string, any>;
}) => React.ReactNode)[];
boxStyle?: CSSProperties;
};
export default function Editor({
@ -100,10 +102,12 @@ export default function Editor({
onBlur,
value,
placeholder = '',
placeholderPadding = '12px 14px',
bg = 'white',
isInvalid,
ExtensionPopover
onKeyDown,
ExtensionPopover,
boxStyle
}: EditorProps &
FormPropsType & {
onOpenModal?: () => void;
@ -180,13 +184,14 @@ export default function Editor({
className={`${isInvalid ? styles.contentEditable_invalid : styles.contentEditable} ${styles.richText}`}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`
maxHeight: `${maxH}px`,
...boxStyle
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
/>
}
placeholder={<Placeholder>{placeholder}</Placeholder>}
placeholder={<Placeholder padding={placeholderPadding}>{placeholder}</Placeholder>}
ErrorBoundary={LexicalErrorBoundary}
/>
) : (
@ -196,11 +201,12 @@ export default function Editor({
className={isInvalid ? styles.contentEditable_invalid : styles.contentEditable}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`
maxHeight: `${maxH}px`,
...boxStyle
}}
/>
}
placeholder={<Placeholder>{placeholder}</Placeholder>}
placeholder={<Placeholder padding={placeholderPadding}>{placeholder}</Placeholder>}
ErrorBoundary={LexicalErrorBoundary}
/>
)}
@ -210,6 +216,7 @@ export default function Editor({
<HistoryPlugin />
<MaxLengthPlugin maxLength={maxLength || 999999} />
<FocusPlugin focus={focus} setFocus={setFocus} />
<KeyDownPlugin onKeyDown={onKeyDown} />
<VariablePlugin variables={variables} />
{variableLabels.length > 0 && (

Some files were not shown because too many files have changed in this diff Show More