Compare commits

...

16 Commits

Author SHA1 Message Date
Archer
09205e4666 fix: price page init data;perf: usage code;fix: reasoning tokens;fix: workflow basic node cannot upgrade (#3816)
* fix: img read

* fix: price page init data

* perf: ai model avatar

* perf: refresh in change team

* perf: null checker

* perf: usage code

* fix: reasoning tokens

* fix: workflow basic node cannot upgrade

* perf: model refresh

* perf: icon refresh
2025-02-18 20:50:25 +08:00
Finley Ge
ccf28d83b8 fix: app version addSourcemember tmbid could be empty (#3822) 2025-02-18 20:26:49 +08:00
LGiki
420aaad48e chore: fix typo in docs (#3819) 2025-02-18 20:25:51 +08:00
heheer
8ba2339890 download fetch baseurl & node select dnd (#3820) 2025-02-18 20:25:15 +08:00
Archer
e7b8934367 Update 4818.md (#3818) 2025-02-18 14:26:21 +08:00
Finley Ge
3e13397614 fix: refresh memberlist when switching account (#3814) 2025-02-18 13:54:56 +08:00
Archer
b14674cc6f fix: whisper checker;fix: img read (#3813)
* fix: img read

* fix: whisper checker

* perf: dev doc

* perf: dev doc

* remove invalid code
2025-02-18 10:08:25 +08:00
Archer
4d20274a97 feat: think tag parse (#3805) (#3808)
* feat: think tag parse

* remove some model config

* feat: parse think tag test
2025-02-17 20:57:36 +08:00
heheer
4447e40364 fix template market simple app (#3804) 2025-02-17 20:56:46 +08:00
John Chen
23949230ee fix document (#3806)
V2版本“获取集合列表”接口的path区分了大小写,使用/api/core/dataset/collection/listv2会返回404,必须使用大写V
2025-02-17 20:55:34 +08:00
saikidev
cd7a897304 chore: add ppio provider (#3789) 2025-02-14 17:04:43 +08:00
Archer
18aff8b8db update yml version (#3787) 2025-02-14 12:50:54 +08:00
Archer
d2b60ec785 fix: model check circle tip (#3786)
* model config

* feat: normalization embedding

* remove log

* version doc

* version doc

* fix: model check circle tip

* uml
2025-02-14 11:42:14 +08:00
a.e.
1226fe42a1 fix: skip thirdparty sso state verification (#3721) (#3782) 2025-02-14 11:39:34 +08:00
Finley Ge
abd375cdec fix: app/dataset list api return private flag (#3784) 2025-02-14 11:38:48 +08:00
Archer
7aacce8b0b 4.9.0 test (#3779)
* model config

* feat: normalization embedding

* remove log

* version doc

* version doc
2025-02-13 16:27:41 +08:00
73 changed files with 928 additions and 442 deletions

View File

@@ -15,8 +15,8 @@ weight: 705
- [Git](http://git-scm.com/)
- [Docker](https://www.docker.com/)(构建镜像)
- [Node.js v18.17 / v20.x](http://nodejs.org)版本尽量一样可以使用nvm管理node版本
- [pnpm](https://pnpm.io/) 版本 8.6.0 (目前官方的开发环境)
- [Node.js v20.14.0](http://nodejs.org)版本尽量一样可以使用nvm管理node版本
- [pnpm](https://pnpm.io/) 推荐版本 9.4.0 (目前官方的开发环境)
- make命令: 根据不同平台,百度安装 (官方是GNU Make 4.3)
## 开始本地开发
@@ -77,8 +77,6 @@ Mongo 数据库需要注意,需要注意在连接地址中增加 `directConnec
可参考项目根目录下的 `dev.md`,第一次编译运行可能会有点慢,需要点耐心哦
```bash
# 给自动化脚本代码执行权限(非 linux 系统, 可以手动执行里面的 postinstall.sh 文件内容)
chmod -R +x ./scripts/
# 代码根目录下执行,会安装根 package、projects 和 packages 内所有依赖
# 如果提示 isolate-vm 安装失败可以参考https://github.com/laverdet/isolated-vm?tab=readme-ov-file#requirements
pnpm i

View File

@@ -43,8 +43,7 @@ weight: 744
{{% alert icon="🤖 " context="success" %}}
注意:
1. 目前语音识别模型和重排模型仅会生效一个,所以配置时候,只需要配置一个即可。
2. 系统必须至少有一个语言模型和一个索引模型才能正常使用。
3. 使用知识库功能,至少要有一个语言模型,用于知识库文件处理(可以在模型配置时候打开该开关),否则知识库会报错。
2. 系统至少需要一个语言模型和一个索引模型才能正常使用。
{{% /alert %}}
#### 核心配置

View File

@@ -735,7 +735,7 @@ data 为集合的 ID。
**4.8.19+**
```bash
curl --location --request POST 'http://localhost:3000/api/core/dataset/collection/listv2' \
curl --location --request POST 'http://localhost:3000/api/core/dataset/collection/listV2' \
--header 'Authorization: Bearer {{authorization}}' \
--header 'Content-Type: application/json' \
--data-raw '{

View File

@@ -1,5 +1,5 @@
---
title: 'V4.8.18'
title: 'V4.8.18(包含升级脚本)'
description: 'FastGPT V4.8.18 更新说明'
icon: 'upgrade'
draft: false

View File

@@ -1,13 +1,21 @@
---
title: 'V4.8.21(进行中)'
title: 'V4.8.21'
description: 'FastGPT V4.8.21 更新说明'
icon: 'upgrade'
draft: false
toc: true
weight: 804
weight: 803
---
## 更新指南
### 1. 做好数据库备份
### 2. 更新镜像:
- 更新 fastgpt 镜像 tag: v4.8.21-fix
- 更新 fastgpt-pro 商业版镜像 tag: v4.8.21-fix
- Sandbox 镜像无需更新
## 完整更新内容

View File

@@ -0,0 +1,23 @@
---
title: 'V4.8.22(进行中)'
description: 'FastGPT V4.8.22 更新说明'
icon: 'upgrade'
draft: false
toc: true
weight: 802
---
## 完整更新内容
1. 新增 - AI 对话节点解析 <think></think> 标签内容,便于各类模型进行思考链输出。
2. 优化 - 模型未配置时提示,减少冲突提示。
3. 优化 - 使用记录代码。
4. 修复 - 思考内容未进入到输出 Tokens.
5. 修复 - 思考链流输出时,有时与正文顺序偏差。
6. 修复 - API 调用工作流,如果传递的图片不支持 Head 检测时,图片会被过滤。已增加该类错误检测,避免被错误过滤。
7. 修复 - 模板市场部分模板错误。
8. 修复 - 免登录窗口无法正常判断语言识别是否开启。
9. 修复 - 对话日志导出,未兼容 sub path。
10. 修复 - list 接口在联查 member 时,存在空指针可能性。
11. 修复 - 工作流基础节点无法升级。

View File

@@ -20,7 +20,7 @@ weight: 502
![](/imgs/fastgpt-api1.jpg)
{{% alert icon="🍅" context="success" %}}
Tips: 安全起见,你可以设置一个额度或者过期时间,放置 key 被滥用。
Tips: 安全起见,你可以设置一个额度或者过期时间,防止 key 被滥用。
{{% /alert %}}

View File

@@ -114,15 +114,15 @@ services:
# fastgpt
sandbox:
container_name: sandbox
image: ghcr.io/labring/fastgpt-sandbox:v4.8.20-fix2 # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.8.20-fix2 # 阿里云
image: ghcr.io/labring/fastgpt-sandbox:v4.8.21-fix # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.8.21-fix # 阿里云
networks:
- fastgpt
restart: always
fastgpt:
container_name: fastgpt
image: ghcr.io/labring/fastgpt:v4.8.20-fix2 # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.20-fix2 # 阿里云
image: ghcr.io/labring/fastgpt:v4.8.21-fix # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.21-fix # 阿里云
ports:
- 3000:3000
networks:

View File

@@ -72,15 +72,15 @@ services:
# fastgpt
sandbox:
container_name: sandbox
image: ghcr.io/labring/fastgpt-sandbox:v4.8.20-fix2 # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.8.20-fix2 # 阿里云
image: ghcr.io/labring/fastgpt-sandbox:v4.8.21-fix # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.8.21-fix # 阿里云
networks:
- fastgpt
restart: always
fastgpt:
container_name: fastgpt
image: ghcr.io/labring/fastgpt:v4.8.20-fix2 # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.20-fix2 # 阿里云
image: ghcr.io/labring/fastgpt:v4.8.21-fix # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.21-fix # 阿里云
ports:
- 3000:3000
networks:

View File

@@ -53,15 +53,15 @@ services:
wait $$!
sandbox:
container_name: sandbox
image: ghcr.io/labring/fastgpt-sandbox:v4.8.20-fix2 # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.8.20-fix2 # 阿里云
image: ghcr.io/labring/fastgpt-sandbox:v4.8.21-fix # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.8.21-fix # 阿里云
networks:
- fastgpt
restart: always
fastgpt:
container_name: fastgpt
image: ghcr.io/labring/fastgpt:v4.8.20-fix2 # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.20-fix2 # 阿里云
image: ghcr.io/labring/fastgpt:v4.8.21-fix # git
# image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.21-fix # 阿里云
ports:
- 3000:3000
networks:

View File

@@ -7,7 +7,7 @@
"format-code": "prettier --config \"./.prettierrc.js\" --write \"./**/src/**/*.{ts,tsx,scss}\"",
"format-doc": "zhlint --dir ./docSite *.md --fix",
"gen:theme-typings": "chakra-cli tokens packages/web/styles/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts",
"postinstall": "sh ./scripts/postinstall.sh",
"postinstall": "pnpm gen:theme-typings",
"initIcon": "node ./scripts/icon/init.js",
"previewIcon": "node ./scripts/icon/index.js",
"api:gen": "tsc ./scripts/openapi/index.ts && node ./scripts/openapi/index.js && npx @redocly/cli build-docs ./scripts/openapi/openapi.json -o ./projects/app/public/openapi/index.html",

View File

@@ -20,4 +20,4 @@ export const ReadFileBaseUrl = `${process.env.FILE_DOMAIN || process.env.FE_DOMA
export const documentFileType = '.txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx';
export const imageFileType =
'.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg, .tiff, .tif, .ico, .heic, .heif, .avif';
'.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg, .tiff, .tif, .ico, .heic, .heif, .avif, .raw, .cr2, .nef, .arw, .dng, .psd, .ai, .eps, .emf, .wmf, .jfif, .exif, .pgm, .ppm, .pbm, .jp2, .j2k, .jpf, .jpx, .jpm, .mj2, .xbm, .pcx';

View File

@@ -1,5 +1,5 @@
import { detect } from 'jschardet';
import { documentFileType, imageFileType } from './constants';
import { documentFileType } from './constants';
import { ChatFileTypeEnum } from '../../core/chat/constants';
import { UserChatItemValueItemType } from '../../core/chat/type';
import * as fs from 'fs';
@@ -25,6 +25,7 @@ export const detectFileEncodingByPath = async (path: string) => {
const fd = await fs.promises.open(path, 'r');
try {
// Read file head
// @ts-ignore
const { bytesRead } = await fd.read(buffer, 0, MAX_BYTES, 0);
const actualBuffer = buffer.slice(0, bytesRead);
@@ -37,40 +38,49 @@ export const detectFileEncodingByPath = async (path: string) => {
// Url => user upload file type
export const parseUrlToFileType = (url: string): UserChatItemValueItemType['file'] | undefined => {
if (typeof url !== 'string') return;
const parseUrl = new URL(url, 'https://locaohost:3000');
const filename = (() => {
// Check base64 image
if (url.startsWith('data:image/')) {
const mime = url.split(',')[0].split(':')[1].split(';')[0];
return `image.${mime.split('/')[1]}`;
}
// Old version file url: https://xxx.com/file/read?filename=xxx.pdf
const filenameQuery = parseUrl.searchParams.get('filename');
if (filenameQuery) return filenameQuery;
// Handle base64 image
if (url.startsWith('data:')) {
const matches = url.match(/^data:([^;]+);base64,/);
if (!matches) return;
// Common file https://xxx.com/xxx.pdf?xxxx=xxx
const pathname = parseUrl.pathname;
if (pathname) return pathname.split('/').pop();
})();
const mimeType = matches[1].toLowerCase();
if (!mimeType.startsWith('image/')) return;
if (!filename) return;
const extension = filename.split('.').pop()?.toLowerCase() || '';
if (!extension) return;
if (documentFileType.includes(extension)) {
const extension = mimeType.split('/')[1];
return {
type: ChatFileTypeEnum.file,
name: filename,
type: ChatFileTypeEnum.image,
name: `image.${extension}`,
url
};
}
if (imageFileType.includes(extension)) {
try {
const parseUrl = new URL(url, 'https://localhost:3000');
// Get filename from URL
const filename = parseUrl.searchParams.get('filename') || parseUrl.pathname.split('/').pop();
const extension = filename?.split('.').pop()?.toLowerCase() || '';
// If it's a document type, return as file, otherwise treat as image
if (extension && documentFileType.includes(extension)) {
return {
type: ChatFileTypeEnum.file,
name: filename || 'null',
url
};
}
// Default to image type for non-document files
return {
type: ChatFileTypeEnum.image,
name: filename,
name: filename || 'null.png',
url
};
} catch (error) {
return {
type: ChatFileTypeEnum.image,
name: 'invalid.png',
url
};
}

View File

@@ -22,6 +22,7 @@ export type ModelProviderIdType =
| 'StepFun'
| 'Yi'
| 'Siliconflow'
| 'PPIO'
| 'Ollama'
| 'BAAI'
| 'FishAudio'
@@ -167,6 +168,11 @@ export const ModelProviderList: ModelProviderType[] = [
name: i18nT('common:model_siliconflow'),
avatar: 'model/siliconflow'
},
{
id: 'PPIO',
name: i18nT('common:model_ppio'),
avatar: 'model/ppio'
},
{
id: 'Other',
name: i18nT('common:model_other'),

View File

@@ -1,14 +1,12 @@
import openai from 'openai';
import type {
ChatCompletionMessageToolCall,
ChatCompletionChunk,
ChatCompletionMessageParam as SdkChatCompletionMessageParam,
ChatCompletionToolMessageParam,
ChatCompletionContentPart as SdkChatCompletionContentPart,
ChatCompletionUserMessageParam as SdkChatCompletionUserMessageParam,
ChatCompletionToolMessageParam as SdkChatCompletionToolMessageParam,
ChatCompletionAssistantMessageParam as SdkChatCompletionAssistantMessageParam,
ChatCompletionContentPartText
ChatCompletionAssistantMessageParam as SdkChatCompletionAssistantMessageParam
} from 'openai/resources';
import { ChatMessageTypeEnum } from './constants';
import { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type';
@@ -48,6 +46,7 @@ export type ChatCompletionMessageParam = (
| CustomChatCompletionToolMessageParam
| CustomChatCompletionAssistantMessageParam
) & {
reasoning_text?: string;
dataId?: string;
hideInUI?: boolean;
};
@@ -71,7 +70,8 @@ export type ChatCompletionMessageFunctionCall =
};
// Stream response
export type StreamChatType = Stream<ChatCompletionChunk>;
export type StreamChatType = Stream<openai.Chat.Completions.ChatCompletionChunk>;
export type UnStreamChatType = openai.Chat.Completions.ChatCompletion;
export default openai;
export * from 'openai';

View File

@@ -46,7 +46,16 @@ export const chats2GPTMessages = ({
messages.forEach((item) => {
const dataId = reserveId ? item.dataId : undefined;
if (item.obj === ChatRoleEnum.Human) {
if (item.obj === ChatRoleEnum.System) {
const content = item.value?.[0]?.text?.content;
if (content) {
results.push({
dataId,
role: ChatCompletionRequestMessageRoleEnum.System,
content
});
}
} else if (item.obj === ChatRoleEnum.Human) {
const value = item.value
.map((item) => {
if (item.type === ChatItemValueTypeEnum.text) {
@@ -80,15 +89,6 @@ export const chats2GPTMessages = ({
role: ChatCompletionRequestMessageRoleEnum.User,
content: simpleUserContentPart(value)
});
} else if (item.obj === ChatRoleEnum.System) {
const content = item.value?.[0]?.text?.content;
if (content) {
results.push({
dataId,
role: ChatCompletionRequestMessageRoleEnum.System,
content
});
}
} else {
const aiResults: ChatCompletionMessageParam[] = [];
@@ -349,7 +349,7 @@ export const chatValue2RuntimePrompt = (value: ChatItemValueItemType[]): Runtime
};
value.forEach((item) => {
if (item.type === 'file' && item.file) {
prompt.files?.push(item.file);
prompt.files.push(item.file);
} else if (item.text) {
prompt.text += item.text.content;
}

View File

@@ -10,6 +10,7 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
import { replaceVariable, valToStr } from '../../../common/string/tools';
import { ChatCompletionChunk } from 'openai/resources';
export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => {
let limit = 10;
@@ -419,3 +420,137 @@ export function rewriteNodeOutputByHistories(
};
});
}
// Parse <think></think> tags to think and answer - unstream response
export const parseReasoningContent = (text: string): [string, string] => {
const regex = /<think>([\s\S]*?)<\/think>/;
const match = text.match(regex);
if (!match) {
return ['', text];
}
const thinkContent = match[1].trim();
// Add answer (remaining text after think tag)
const answerContent = text.slice(match.index! + match[0].length);
return [thinkContent, answerContent];
};
// Parse <think></think> tags to think and answer - stream response
export const parseReasoningStreamContent = () => {
let isInThinkTag: boolean | undefined;
const startTag = '<think>';
let startTagBuffer = '';
const endTag = '</think>';
let endTagBuffer = '';
/*
parseReasoning - 只控制是否主动解析 <think></think>,如果接口已经解析了,仍然会返回 think 内容。
*/
const parsePart = (
part: {
choices: {
delta: {
content?: string;
reasoning_content?: string;
};
}[];
},
parseReasoning = false
): [string, string] => {
const content = part.choices?.[0]?.delta?.content || '';
// @ts-ignore
const reasoningContent = part.choices?.[0]?.delta?.reasoning_content || '';
if (reasoningContent || !parseReasoning) {
isInThinkTag = false;
return [reasoningContent, content];
}
if (!content) {
return ['', ''];
}
// 如果不在 think 标签中,或者有 reasoningContent(接口已解析),则返回 reasoningContent 和 content
if (isInThinkTag === false) {
return ['', content];
}
// 检测是否为 think 标签开头的数据
if (isInThinkTag === undefined) {
// Parse content think and answer
startTagBuffer += content;
// 太少内容时候,暂时不解析
if (startTagBuffer.length < startTag.length) {
return ['', ''];
}
if (startTagBuffer.startsWith(startTag)) {
isInThinkTag = true;
return [startTagBuffer.slice(startTag.length), ''];
}
// 如果未命中 think 标签,则认为不在 think 标签中,返回 buffer 内容作为 content
isInThinkTag = false;
return ['', startTagBuffer];
}
// 确认是 think 标签内容,开始返回 think 内容,并实时检测 </think>
/*
检测 </think> 方案。
存储所有疑似 </think> 的内容,直到检测到完整的 </think> 标签或超出 </think> 长度。
content 返回值包含以下几种情况:
abc - 完全未命中尾标签
abc<th - 命中一部分尾标签
abc</think> - 完全命中尾标签
abc</think>abc - 完全命中尾标签
</think>abc - 完全命中尾标签
k>abc - 命中一部分尾标签
*/
// endTagBuffer 专门用来记录疑似尾标签的内容
if (endTagBuffer) {
endTagBuffer += content;
if (endTagBuffer.includes(endTag)) {
isInThinkTag = false;
const answer = endTagBuffer.slice(endTag.length);
return ['', answer];
} else if (endTagBuffer.length >= endTag.length) {
// 缓存内容超出尾标签长度,且仍未命中 </think>,则认为本次猜测 </think> 失败,仍处于 think 阶段。
const tmp = endTagBuffer;
endTagBuffer = '';
return [tmp, ''];
}
return ['', ''];
} else if (content.includes(endTag)) {
// 返回内容,完整命中</think>,直接结束
isInThinkTag = false;
const [think, answer] = content.split(endTag);
return [think, answer];
} else {
// 无 buffer且未命中 </think>,开始疑似 </think> 检测。
for (let i = 1; i < endTag.length; i++) {
const partialEndTag = endTag.slice(0, i);
// 命中一部分尾标签
if (content.endsWith(partialEndTag)) {
const think = content.slice(0, -partialEndTag.length);
endTagBuffer += partialEndTag;
return [think, ''];
}
}
}
// 完全未命中尾标签,还是 think 阶段。
return [content, ''];
};
const getStartTagBuffer = () => startTagBuffer;
return {
parsePart,
getStartTagBuffer
};
};

View File

@@ -26,15 +26,18 @@ export async function uploadMongoImg({
const [base64Mime, base64Data] = base64Img.split(',');
// Check if mime type is valid
if (!base64MimeRegex.test(base64Mime)) {
return Promise.reject('Invalid image mime type');
return Promise.reject('Invalid image base64');
}
const mime = `image/${base64Mime.match(base64MimeRegex)?.[1] ?? 'image/jpeg'}`;
const binary = Buffer.from(base64Data, 'base64');
const extension = mime.split('/')[1];
let extension = mime.split('/')[1];
if (extension.startsWith('x-')) {
extension = extension.substring(2); // Remove 'x-' prefix
}
if (!imageFileType.includes(`.${extension}`)) {
return Promise.reject('Invalid image file type');
if (!extension || !imageFileType.includes(`.${extension}`)) {
return Promise.reject(`Invalid image file type: ${mime}`);
}
const { _id } = await MongoImage.create({

View File

@@ -25,7 +25,7 @@ export const countGptMessagesTokens = async (
number
>({
name: WorkerNameEnum.countGptMessagesTokens,
maxReservedThreads: global.systemEnv?.tokenWorkers || 50
maxReservedThreads: global.systemEnv?.tokenWorkers || 30
});
const total = await workerController.run({ messages, tools, functionCall });

View File

@@ -1,7 +1,9 @@
import OpenAI from '@fastgpt/global/core/ai';
import {
ChatCompletionCreateParamsNonStreaming,
ChatCompletionCreateParamsStreaming
ChatCompletionCreateParamsStreaming,
StreamChatType,
UnStreamChatType
} from '@fastgpt/global/core/ai/type';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { addLog } from '../../common/system/log';
@@ -38,29 +40,30 @@ export const getAxiosConfig = (props?: { userKey?: OpenaiAccountType }) => {
};
};
type CompletionsBodyType =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming;
type InferResponseType<T extends CompletionsBodyType> =
T extends ChatCompletionCreateParamsStreaming
? OpenAI.Chat.Completions.ChatCompletionChunk
: OpenAI.Chat.Completions.ChatCompletion;
export const createChatCompletion = async <T extends CompletionsBodyType>({
export const createChatCompletion = async ({
body,
userKey,
timeout,
options
}: {
body: T;
body: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming;
userKey?: OpenaiAccountType;
timeout?: number;
options?: OpenAI.RequestOptions;
}): Promise<{
response: InferResponseType<T>;
isStreamResponse: boolean;
getEmptyResponseTip: () => string;
}> => {
}): Promise<
{
getEmptyResponseTip: () => string;
} & (
| {
response: StreamChatType;
isStreamResponse: true;
}
| {
response: UnStreamChatType;
isStreamResponse: false;
}
)
> => {
try {
const modelConstantsData = getLLMModel(body.model);
@@ -96,9 +99,17 @@ export const createChatCompletion = async <T extends CompletionsBodyType>({
return i18nT('chat:LLM_model_response_empty');
};
if (isStreamResponse) {
return {
response,
isStreamResponse: true,
getEmptyResponseTip
};
}
return {
response: response as InferResponseType<T>,
isStreamResponse,
response,
isStreamResponse: false,
getEmptyResponseTip
};
} catch (error) {

View File

@@ -0,0 +1,4 @@
{
"provider": "PPIO",
"list": []
}

View File

@@ -37,25 +37,26 @@ export const computedTemperature = ({
return temperature;
};
type CompletionsBodyType = (
type CompletionsBodyType =
| ChatCompletionCreateParamsNonStreaming
| ChatCompletionCreateParamsStreaming
) & {
response_format?: any;
json_schema?: string;
stop?: string;
};
| ChatCompletionCreateParamsStreaming;
type InferCompletionsBody<T> = T extends { stream: true }
? ChatCompletionCreateParamsStreaming
: ChatCompletionCreateParamsNonStreaming;
: T extends { stream: false }
? ChatCompletionCreateParamsNonStreaming
: ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming;
export const llmCompletionsBodyFormat = <T extends CompletionsBodyType>(
body: T,
body: T & {
response_format?: any;
json_schema?: string;
stop?: string;
},
model: string | LLMModelItemType
): InferCompletionsBody<T> => {
const modelData = typeof model === 'string' ? getLLMModel(model) : model;
if (!modelData) {
return body as InferCompletionsBody<T>;
return body as unknown as InferCompletionsBody<T>;
}
const response_format = body.response_format;
@@ -91,9 +92,7 @@ export const llmCompletionsBodyFormat = <T extends CompletionsBodyType>(
});
}
// console.log(requestBody);
return requestBody as InferCompletionsBody<T>;
return requestBody as unknown as InferCompletionsBody<T>;
};
export const llmStreamResponseToText = async (response: StreamChatType) => {

View File

@@ -197,7 +197,11 @@ export const loadRequestMessages = async ({
addLog.info(`Filter invalid image: ${imgUrl}`);
return;
}
} catch (error) {
} catch (error: any) {
if (error?.response?.status === 405) {
return item;
}
addLog.warn(`Filter invalid image: ${imgUrl}`, { error });
return;
}
}

View File

@@ -334,7 +334,7 @@ const getMultiInput = async ({
return {
documentQuoteText: text,
userFiles: fileLinks.map((url) => parseUrlToFileType(url))
userFiles: fileLinks.map((url) => parseUrlToFileType(url)).filter(Boolean)
};
};

View File

@@ -3,13 +3,13 @@ import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../chat
import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import {
parseReasoningContent,
parseReasoningStreamContent,
textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils';
import { createChatCompletion } from '../../../ai/config';
import type {
ChatCompletion,
ChatCompletionMessageParam,
StreamChatType
} from '@fastgpt/global/core/ai/type.d';
import type { ChatCompletionMessageParam, StreamChatType } from '@fastgpt/global/core/ai/type.d';
import { formatModelChars2Points } from '../../../../support/wallet/usage/utils';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { postTextCensor } from '../../../../common/api/requestPlusApi';
@@ -195,7 +195,13 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
});
const { answerText, reasoningText } = await (async () => {
if (res && isStreamResponse) {
if (isStreamResponse) {
if (!res) {
return {
answerText: '',
reasoningText: ''
};
}
// sse response
const { answer, reasoning } = await streamResponse({
res,
@@ -210,34 +216,49 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
reasoningText: reasoning
};
} else {
const unStreamResponse = response as ChatCompletion;
const answer = unStreamResponse.choices?.[0]?.message?.content || '';
// @ts-ignore
const reasoning = unStreamResponse.choices?.[0]?.message?.reasoning_content || '';
const { content, reasoningContent } = (() => {
const content = response.choices?.[0]?.message?.content || '';
// @ts-ignore
const reasoningContent: string = response.choices?.[0]?.message?.reasoning_content || '';
// API already parse reasoning content
if (reasoningContent || !aiChatReasoning) {
return {
content,
reasoningContent
};
}
const [think, answer] = parseReasoningContent(content);
return {
content: answer,
reasoningContent: think
};
})();
// Some models do not support streaming
if (stream) {
if (isResponseAnswerText && answer) {
if (aiChatReasoning && reasoningContent) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
text: answer
reasoning_content: reasoningContent
})
});
}
if (aiChatReasoning && reasoning) {
if (isResponseAnswerText && content) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
reasoning_content: reasoning
text: content
})
});
}
}
return {
answerText: answer,
reasoningText: reasoning
answerText: content,
reasoningText: reasoningContent
};
}
})();
@@ -249,7 +270,8 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
const AIMessages: ChatCompletionMessageParam[] = [
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: answerText
content: answerText,
reasoning_text: reasoningText // reasoning_text is only recorded for response, but not for request
}
];
@@ -267,7 +289,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
});
return {
answerText,
answerText: answerText.trim(),
reasoningText,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: externalProvider.openaiAccount?.key ? 0 : totalPoints,
@@ -386,7 +408,7 @@ async function getMultiInput({
return {
documentQuoteText: text,
userFiles: fileLinks.map((url) => parseUrlToFileType(url))
userFiles: fileLinks.map((url) => parseUrlToFileType(url)).filter(Boolean)
};
}
@@ -500,26 +522,18 @@ async function streamResponse({
});
let answer = '';
let reasoning = '';
const { parsePart, getStartTagBuffer } = parseReasoningStreamContent();
for await (const part of stream) {
if (res.closed) {
stream.controller?.abort();
break;
}
const content = part.choices?.[0]?.delta?.content || '';
const [reasoningContent, content] = parsePart(part, aiChatReasoning);
answer += content;
if (isResponseAnswerText && content) {
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: content
})
});
}
const reasoningContent = part.choices?.[0]?.delta?.reasoning_content || '';
reasoning += reasoningContent;
if (aiChatReasoning && reasoningContent) {
workflowStreamResponse?.({
write,
@@ -529,6 +543,21 @@ async function streamResponse({
})
});
}
if (isResponseAnswerText && content) {
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: content
})
});
}
}
// if answer is empty, try to get value from startTagBuffer. (Cause: The response content is too short to exceed the minimum parse length)
if (answer === '') {
answer = getStartTagBuffer();
}
return { answer, reasoning };

View File

@@ -232,9 +232,14 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
chatNodeUsages = chatNodeUsages.concat(nodeDispatchUsages);
}
if (toolResponses !== undefined) {
if (toolResponses !== undefined && toolResponses !== null) {
if (Array.isArray(toolResponses) && toolResponses.length === 0) return;
if (typeof toolResponses === 'object' && Object.keys(toolResponses).length === 0) return;
if (
!Array.isArray(toolResponses) &&
typeof toolResponses === 'object' &&
Object.keys(toolResponses).length === 0
)
return;
toolRunResponse = toolResponses;
}
@@ -243,12 +248,17 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
chatAssistantResponse = chatAssistantResponse.concat(assistantResponses);
} else {
if (reasoningText) {
chatAssistantResponse.push({
type: ChatItemValueTypeEnum.reasoning,
reasoning: {
content: reasoningText
}
});
const isResponseReasoningText = inputs.find(
(item) => item.key === NodeInputKeyEnum.aiChatReasoning
)?.value;
if (isResponseReasoningText) {
chatAssistantResponse.push({
type: ChatItemValueTypeEnum.reasoning,
reasoning: {
content: reasoningText
}
});
}
}
if (answerText) {
// save assistant text response

View File

@@ -53,7 +53,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
const userInputFiles = (() => {
if (fileUrlList) {
return fileUrlList.map((url) => parseUrlToFileType(url));
return fileUrlList.map((url) => parseUrlToFileType(url)).filter(Boolean);
}
// Adapt version 4.8.13 upgrade
return files;

View File

@@ -398,41 +398,6 @@ async function fetchData({
};
}
// function replaceVariable(text: string, obj: Record<string, any>) {
// for (const [key, value] of Object.entries(obj)) {
// if (value === undefined) {
// text = text.replace(new RegExp(`{{(${key})}}`, 'g'), UNDEFINED_SIGN);
// } else {
// const replacement = JSON.stringify(value);
// const unquotedReplacement =
// replacement.startsWith('"') && replacement.endsWith('"')
// ? replacement.slice(1, -1)
// : replacement;
// text = text.replace(new RegExp(`{{(${key})}}`, 'g'), () => unquotedReplacement);
// }
// }
// return text || '';
// }
// function removeUndefinedSign(obj: Record<string, any>) {
// for (const key in obj) {
// if (obj[key] === UNDEFINED_SIGN) {
// obj[key] = undefined;
// } else if (Array.isArray(obj[key])) {
// obj[key] = obj[key].map((item: any) => {
// if (item === UNDEFINED_SIGN) {
// return undefined;
// } else if (typeof item === 'object') {
// removeUndefinedSign(item);
// }
// return item;
// });
// } else if (typeof obj[key] === 'object') {
// removeUndefinedSign(obj[key]);
// }
// }
// return obj;
// }
// Replace some special response from system plugin
async function replaceSystemPluginResponse({
response,

View File

@@ -86,9 +86,12 @@ export async function addSourceMember<T extends { tmbId: string }>({
}): Promise<Array<T & { sourceMember: SourceMemberType }>> {
if (!Array.isArray(list)) return [];
const tmbIdList = list
.map((item) => (item.tmbId ? String(item.tmbId) : undefined))
.filter(Boolean);
const tmbList = await MongoTeamMember.find(
{
_id: { $in: list.map((item) => String(item.tmbId)) }
_id: { $in: tmbIdList }
},
'tmbId name avatar status',
{

View File

@@ -1,6 +1,114 @@
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { MongoUsage } from './schema';
import { ClientSession } from '../../../common/mongo';
import { ClientSession, Types } from '../../../common/mongo';
import { addLog } from '../../../common/system/log';
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { ConcatUsageProps, CreateUsageProps } from '@fastgpt/global/support/wallet/usage/api';
import { i18nT } from '../../../../web/i18n/utils';
import { pushConcatBillTask, pushReduceTeamAiPointsTask } from './utils';
import { POST } from '../../../common/api/plusRequest';
import { FastGPTProUrl } from '../../../common/system/constants';
export async function createUsage(data: CreateUsageProps) {
try {
// In FastGPT server
if (FastGPTProUrl) {
await POST('/support/wallet/usage/createUsage', data);
} else if (global.reduceAiPointsQueue) {
// In FastGPT pro server
await MongoUsage.create(data);
pushReduceTeamAiPointsTask({ teamId: data.teamId, totalPoints: data.totalPoints });
if (data.totalPoints === 0) {
addLog.info('0 totalPoints', data);
}
}
} catch (error) {
addLog.error('createUsage error', error);
}
}
export async function concatUsage(data: ConcatUsageProps) {
try {
// In FastGPT server
if (FastGPTProUrl) {
await POST('/support/wallet/usage/concatUsage', data);
} else if (global.reduceAiPointsQueue) {
const {
teamId,
billId,
totalPoints = 0,
listIndex,
inputTokens = 0,
outputTokens = 0
} = data;
// billId is required and valid
if (!billId || !Types.ObjectId.isValid(billId)) return;
// In FastGPT pro server
pushConcatBillTask([
{
billId,
listIndex,
inputTokens,
outputTokens,
totalPoints
}
]);
pushReduceTeamAiPointsTask({ teamId, totalPoints });
if (data.totalPoints === 0) {
addLog.info('0 totalPoints', data);
}
}
} catch (error) {
addLog.error('concatUsage error', error);
}
}
export const createChatUsage = ({
appName,
appId,
pluginId,
teamId,
tmbId,
source,
flowUsages
}: {
appName: string;
appId?: string;
pluginId?: string;
teamId: string;
tmbId: string;
source: UsageSourceEnum;
flowUsages: ChatNodeUsageType[];
}) => {
const totalPoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0);
createUsage({
teamId,
tmbId,
appName,
appId,
pluginId,
totalPoints,
source,
list: flowUsages.map((item) => ({
moduleName: item.moduleName,
amount: item.totalPoints || 0,
model: item.model,
inputTokens: item.inputTokens,
outputTokens: item.outputTokens
}))
});
addLog.debug(`Create chat usage`, {
source,
teamId,
totalPoints
});
return { totalPoints };
};
export const createTrainingUsage = async ({
teamId,
@@ -29,21 +137,21 @@ export const createTrainingUsage = async ({
totalPoints: 0,
list: [
{
moduleName: 'support.wallet.moduleName.index',
moduleName: i18nT('common:support.wallet.moduleName.index'),
model: vectorModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
moduleName: 'support.wallet.moduleName.qa',
moduleName: i18nT('common:support.wallet.moduleName.qa'),
model: agentModel,
amount: 0,
inputTokens: 0,
outputTokens: 0
},
{
moduleName: 'core.dataset.training.Auto mode',
moduleName: i18nT('common:core.dataset.training.Auto mode'),
model: agentModel,
amount: 0,
inputTokens: 0,

View File

@@ -0,0 +1,12 @@
export type ConcatBillQueueItemType = {
billId: string;
listIndex?: number;
totalPoints: number;
inputTokens: number;
outputTokens: number;
};
declare global {
var reduceAiPointsQueue: { teamId: string; totalPoints: number }[];
var concatBillQueue: ConcatBillQueueItemType[];
}

View File

@@ -1,5 +1,6 @@
import { findAIModel } from '../../../core/ai/model';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { ConcatBillQueueItemType } from './type';
export const formatModelChars2Points = ({
model,
@@ -34,3 +35,20 @@ export const formatModelChars2Points = ({
totalPoints
};
};
export const pushReduceTeamAiPointsTask = ({
teamId,
totalPoints
}: {
teamId: string;
totalPoints: number;
}) => {
global.reduceAiPointsQueue.push({
teamId: String(teamId),
totalPoints
});
};
export const pushConcatBillTask = (data: ConcatBillQueueItemType[]) => {
global.concatBillQueue.push(...data);
};

View File

@@ -72,7 +72,7 @@ parentPort?.on(
};
const total =
messages.reduce((sum, item) => {
messages.reduce((sum, item, index) => {
// Evaluates the text of toolcall and functioncall
const functionCallPrompt = (() => {
let prompt = '';
@@ -100,7 +100,13 @@ parentPort?.on(
.join('');
})();
return sum + countPromptTokens(`${contentPrompt}${functionCallPrompt}`, item.role);
// Only the last message computed reasoning_text
const reasoningText = index === messages.length - 1 ? item.reasoning_text || '' : '';
return (
sum +
countPromptTokens(`${reasoningText}${contentPrompt}${functionCallPrompt}`, item.role)
);
}, 0) +
countToolsTokens(tools) +
countToolsTokens(functionCall);

View File

@@ -389,6 +389,7 @@ export const iconPaths = {
'model/openai': () => import('./icons/model/openai.svg'),
'model/qwen': () => import('./icons/model/qwen.svg'),
'model/siliconflow': () => import('./icons/model/siliconflow.svg'),
'model/ppio': () => import('./icons/model/ppio.svg'),
'model/sparkDesk': () => import('./icons/model/sparkDesk.svg'),
'model/stepfun': () => import('./icons/model/stepfun.svg'),
'model/yi': () => import('./icons/model/yi.svg'),

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M49.6479 0.359833C22.2415 0.359833 0 22.5703 0 49.9767C0 63.4861 5.41303 75.7546 14.1918 84.7039V50.0233C14.1918 40.5621 17.8832 31.6283 24.568 24.9434C31.2839 18.2275 40.1867 14.5671 49.6634 14.5671H49.9581L49.6479 14.5981C69.2372 14.5981 85.1196 30.4805 85.1196 50.0543C85.1196 51.7604 84.9955 53.4355 84.7628 55.0951L64.7238 34.9939C60.7221 30.9923 55.3556 28.7744 49.6789 28.7744C44.0022 28.7744 38.6512 30.9923 34.6341 34.9939C30.6015 39.0266 28.399 44.3621 28.399 50.0543C28.399 55.7465 30.617 61.082 34.6341 65.1146C38.6357 69.1162 44.0022 71.3342 49.6789 71.3342C55.3556 71.3342 60.7066 69.1162 64.7238 65.1146C68.4617 61.3767 70.6176 56.491 70.9123 51.2641L82.669 63.0673C77.4731 76.2199 64.6617 85.5415 49.6634 85.5415C41.8929 85.5415 34.479 83.0598 28.3835 78.4533V94.863C34.8357 97.934 42.0324 99.6402 49.6324 99.6402C77.0388 99.6402 99.2803 77.4297 99.2803 50.0233C99.3113 22.5858 77.0853 0.375343 49.6634 0.375343L49.6479 0.359833Z" fill="#0062E2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,17 +1,17 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import type { IconProps } from '@chakra-ui/react';
import { Box, Icon } from '@chakra-ui/react';
import { iconPaths } from './constants';
import type { IconNameType } from './type.d';
import { useRefresh } from '../../../hooks/useRefresh';
const iconCache: Record<string, any> = {};
const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType } & IconProps) => {
const [IconComponent, setIconComponent] = useState<any>(null);
const { refresh } = useRefresh();
useEffect(() => {
if (iconCache[name]) {
setIconComponent(iconCache[name]);
return;
}
@@ -20,11 +20,13 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType
const component = { as: icon.default };
// Store in cache
iconCache[name] = component;
setIconComponent(component);
refresh();
})
.catch((error) => console.log(error));
}, [name]);
const IconComponent = iconCache[name];
return !!IconComponent ? (
<Icon
{...IconComponent}
@@ -40,4 +42,4 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType
);
};
export default MyIcon;
export default React.memo(MyIcon);

View File

@@ -23,7 +23,9 @@
"Move": "Move",
"Name": "Name",
"None": "None",
"Operation": "Operation",
"Rename": "Rename",
"Required_input": "Required",
"Resume": "Resume",
"Running": "Running",
"Select_all": "Select all",
@@ -165,7 +167,6 @@
"common.Not open": "Not Open",
"common.OK": "OK",
"common.Open": "Open",
"Operation": "Operation",
"common.Other": "Other",
"common.Output": "Output",
"common.Params": "Parameters",
@@ -179,7 +180,6 @@
"common.Remove": "Remove",
"common.Rename": "Rename",
"common.Request Error": "Request Error",
"Required_input": "Required",
"common.Reset": "Reset",
"common.Restart": "Restart",
"common.Role": "Permission",
@@ -369,7 +369,7 @@
"core.app.tip.Add a intro to app": "Give the app an introduction",
"core.app.tip.chatNodeSystemPromptTip": "Enter a prompt here",
"core.app.tip.systemPromptTip": "Fixed guide words for the model. By adjusting this content, you can guide the model's chat direction. This content will be fixed at the beginning of the context. You can use / to insert variables.\nIf a Dataset is associated, you can also guide the model when to call the Dataset search by appropriate description. For example:\nYou are an assistant for the movie 'Interstellar'. When users ask about content related to 'Interstellar', please search the Dataset and answer based on the search results.",
"core.app.tip.variableTip": "Before the conversation starts, you can ask the user to fill in some content as specific variables for this round of conversation. This module is located after the opening guide.\nVariables can be injected into other modules' string type inputs in the form of {{variable key}}, such as prompts, delimiters, etc.",
"core.app.tip.variableTip": "Before the conversation begins, users can be asked to fill in some content as specific variables for this round of conversation. \nThis module is located after the opening boot.\n\nIn the input box, you can select variables through / activation, such as: prompt words, qualifiers, etc.",
"core.app.tip.welcomeTextTip": "Before each conversation starts, send an initial content. Supports standard Markdown syntax. Additional tags that can be used:\n[Quick Key]: Users can directly send the question by clicking",
"core.app.tool_label.doc": "Documentation",
"core.app.tool_label.github": "GitHub Address",
@@ -878,6 +878,7 @@
"dataset.test.noResult": "No Search Results",
"deep_rag_search": "In-depth search",
"delete_api": "Are you sure you want to delete this API key? \nAfter deletion, the key will become invalid immediately and the corresponding conversation log will not be deleted. Please confirm!",
"embedding_model_not_config": "No index model is detected",
"error.Create failed": "Create failed",
"error.code_error": "Verification code error",
"error.fileNotFound": "File not found~",
@@ -914,6 +915,7 @@
"item_name": "Field Name",
"just_now": "just",
"key_repetition": "Key Repetition",
"llm_model_not_config": "No language model was detected",
"max_quote_tokens": "Quote cap",
"max_quote_tokens_tips": "The maximum number of tokens in a single search, about 1 character in Chinese = 1.7 tokens, and about 1 character in English = 1 token",
"min_similarity": "lowest correlation",
@@ -939,6 +941,7 @@
"model_moka": "Moka-AI",
"model_moonshot": "Moonshot",
"model_other": "Other",
"model_ppio": "PPIO",
"model_qwen": "Qwen",
"model_siliconflow": "Siliconflow",
"model_sparkdesk": "SprkDesk",

View File

@@ -6,7 +6,6 @@
"forget_password": "Find Password",
"login_failed": "Login failed",
"login_success": "Login successful",
"model_not_config": "It is detected that the system has not configured the model, please configure the model before using it",
"no_remind": "Don't remind again",
"password_condition": "Password maximum 60 characters",
"password_tip": "Password must be at least 6 characters long and contain at least two combinations: numbers, letters, or special characters",

View File

@@ -23,7 +23,9 @@
"Move": "移动",
"Name": "名称",
"None": "无",
"Operation": "操作",
"Rename": "重命名",
"Required_input": "必填",
"Resume": "恢复",
"Running": "运行中",
"Select_all": "全选",
@@ -169,7 +171,6 @@
"common.Not open": "未开启",
"common.OK": "好的",
"common.Open": "打开",
"Operation": "操作",
"common.Other": "其他",
"common.Output": "输出",
"common.Params": "参数",
@@ -183,7 +184,6 @@
"common.Remove": "移除",
"common.Rename": "重命名",
"common.Request Error": "请求异常",
"Required_input": "必填",
"common.Reset": "恢复默认",
"common.Restart": "重新开始",
"common.Role": "权限",
@@ -372,7 +372,7 @@
"core.app.tip.Add a intro to app": "快来给应用一个介绍~",
"core.app.tip.chatNodeSystemPromptTip": "在此输入提示词",
"core.app.tip.systemPromptTip": "模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可通过输入 / 插入选择变量\n如果关联了知识库你还可以通过适当的描述来引导模型何时去调用知识库搜索。例如\n你是电影《星际穿越》的助手当用户询问与《星际穿越》相关的内容时请搜索知识库并结合搜索结果进行回答。",
"core.app.tip.variableTip": "可以在对话开始前,要求用户填写一些内容作为本轮对话的特定变量。该模块位于开场引导之后。\n变量可以通过 {{变量key}} 的形式注入到其他模块 string 类型的输入中,例如:提示词、限定词等",
"core.app.tip.variableTip": "可以在对话开始前,要求用户填写一些内容作为本轮对话的特定变量。该模块位于开场引导之后。\n输入框中,可通过 / 激活变量选择,例如:提示词、限定词等",
"core.app.tip.welcomeTextTip": "每次对话开始前,发送一个初始内容。支持标准 Markdown 语法,可使用的额外标记:\n[快捷按键]:用户点击后可以直接发送该问题",
"core.app.tool_label.doc": "使用文档",
"core.app.tool_label.github": "GitHub地址",
@@ -881,6 +881,7 @@
"dataset.test.noResult": "搜索结果为空",
"deep_rag_search": "深度搜索",
"delete_api": "确认删除该API密钥删除后该密钥立即失效对应的对话日志不会删除请确认",
"embedding_model_not_config": "检测到没有可用的索引模型",
"error.Create failed": "创建失败",
"error.code_error": "验证码错误",
"error.fileNotFound": "文件找不到了~",
@@ -917,6 +918,7 @@
"item_name": "字段名",
"just_now": "刚刚",
"key_repetition": "key 重复",
"llm_model_not_config": "检测到没有可用的语言模型",
"max_quote_tokens": "引用上限",
"max_quote_tokens_tips": "单次搜索最大的 token 数量,中文约 1 字=1.7 tokens英文约 1 字=1 token",
"min_similarity": "最低相关度",
@@ -944,6 +946,7 @@
"model_other": "其他",
"model_qwen": "阿里千问",
"model_siliconflow": "硅基流动",
"model_ppio": "PPIO 派欧云",
"model_sparkdesk": "讯飞星火",
"model_stepfun": "阶跃星辰",
"model_yi": "零一万物",

View File

@@ -6,7 +6,6 @@
"forget_password": "忘记密码?",
"login_failed": "登录异常",
"login_success": "登录成功",
"model_not_config": "检测到系统未配置模型,请先配置模型后再使用",
"no_remind": "不再提醒",
"password_condition": "密码最多 60 位",
"password_tip": "密码至少 6 位,且至少包含两种组合:数字、字母或特殊字符",

View File

@@ -23,7 +23,9 @@
"Move": "移動",
"Name": "名稱",
"None": "無",
"Operation": "操作",
"Rename": "重新命名",
"Required_input": "必填",
"Resume": "繼續",
"Running": "執行中",
"Select_all": "全選",
@@ -164,7 +166,6 @@
"common.Not open": "未開啟",
"common.OK": "確定",
"common.Open": "開啟",
"Operation": "操作",
"common.Other": "其他",
"common.Output": "輸出",
"common.Params": "參數",
@@ -178,7 +179,6 @@
"common.Remove": "移除",
"common.Rename": "重新命名",
"common.Request Error": "請求錯誤",
"Required_input": "必填",
"common.Reset": "恢復預設",
"common.Restart": "重新開始",
"common.Role": "權限",
@@ -368,7 +368,7 @@
"core.app.tip.Add a intro to app": "快來為應用程式寫一個介紹",
"core.app.tip.chatNodeSystemPromptTip": "在此輸入提示詞",
"core.app.tip.systemPromptTip": "模型固定的引導詞,透過調整此內容,可以引導模型對話方向。此內容會固定在上下文的開頭。可透過輸入 / 插入變數。\n如果關聯了知識庫您還可以透過適當的描述引導模型何時去呼叫知識庫搜尋。例如\n您是電影《星際效應》的助手當使用者詢問與《星際效應》相關的內容時請搜尋知識庫並根據搜尋結果回答。",
"core.app.tip.variableTip": "可以在對話開始前,要求使用者填寫一些內容作為本輪對話的特定變數。此模組位於開場引導之後。\n變數可以透過 {{變數 key}} 的形式注入到其他模組的字串類型輸入中,例如:提示詞、分隔符等",
"core.app.tip.variableTip": "可以在對話開始前,要求用戶填寫一些內容作為本輪對話的特定變量。\n該模塊位於開場引導之後。\n\n輸入框中可通過 / 激活變量選擇,例如:提示詞、限定詞等",
"core.app.tip.welcomeTextTip": "每次對話開始前,傳送一段初始內容。支援標準 Markdown 語法。可使用的額外標記:\n[快速按鍵]:使用者點選後可以直接傳送該問題",
"core.app.tool_label.doc": "使用文件",
"core.app.tool_label.github": "GitHub 網址",
@@ -878,6 +878,7 @@
"dataset.test.noResult": "搜尋結果為空",
"deep_rag_search": "深度搜索",
"delete_api": "確認刪除此 API 金鑰?\n刪除後該金鑰將立即失效對應的對話記錄不會被刪除請確認",
"embedding_model_not_config": "檢測到沒有可用的索引模型",
"error.Create failed": "建立失敗",
"error.code_error": "驗證碼錯誤",
"error.fileNotFound": "找不到檔案",
@@ -914,6 +915,7 @@
"item_name": "欄位名稱",
"just_now": "剛剛",
"key_repetition": "鍵值重複",
"llm_model_not_config": "檢測到沒有可用的語言模型",
"max_quote_tokens": "引用上限",
"max_quote_tokens_tips": "單次搜尋最大的 token 數量,中文約 1 字=1.7 tokens英文約 1 字=1 token",
"min_similarity": "最低相關度",
@@ -938,6 +940,7 @@
"model_moka": "Moka-AI",
"model_moonshot": "月之暗面",
"model_other": "其他",
"model_ppio": "PPIO 派歐雲",
"model_qwen": "阿里千問",
"model_siliconflow": "矽基流動",
"model_sparkdesk": "訊飛星火",

View File

@@ -6,7 +6,6 @@
"forget_password": "忘記密碼?",
"login_failed": "登入失敗",
"login_success": "登入成功",
"model_not_config": "檢測到系統未配置模型,請先配置模型後再使用",
"no_remind": "不再提醒",
"password_condition": "密碼最多 60 個字元",
"password_tip": "密碼至少 6 位,且至少包含兩種組合:數字、字母或特殊字符",

View File

@@ -6,7 +6,7 @@
"systemEnv": {
"vectorMaxProcess": 15, // 向量处理线程数量
"qaMaxProcess": 15, // 问答拆分线程数量
"tokenWorkers": 50, // Token 计算线程保持数,会持续占用内存,不能设置太大。
"tokenWorkers": 30, // Token 计算线程保持数,会持续占用内存,不能设置太大。
"pgHNSWEfSearch": 100 // 向量搜索参数。越大搜索越精确但是速度越慢。设置为100有99%+精度。
}
}

View File

@@ -6,7 +6,7 @@
"systemEnv": {
"vectorMaxProcess": 15, // 向量处理线程数量
"qaMaxProcess": 15, // 问答拆分线程数量
"tokenWorkers": 50, // Token 计算线程保持数,会持续占用内存,不能设置太大。
"tokenWorkers": 30, // Token 计算线程保持数,会持续占用内存,不能设置太大。
"pgHNSWEfSearch": 100 // 向量搜索参数。越大搜索越精确但是速度越慢。设置为100有99%+精度。
},
"llmModels": [

View File

@@ -9,7 +9,7 @@ module.exports = {
locales: ['en', 'zh-CN', 'zh-Hant'],
localeDetection: false
},
localePath:
typeof window === 'undefined' ? require('path').resolve('../../packages/web/i18n') : '/i18n',
defaultNS: 'common',
localePath: require('path').resolve('../../packages/web/i18n'),
reloadOnPrerender: process.env.NODE_ENV === 'development'
};

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
@@ -11,7 +11,7 @@ import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
import Auth from './auth';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useMount } from 'ahooks';
import { useDebounceEffect, useMount } from 'ahooks';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -88,18 +88,29 @@ const Layout = ({ children }: { children: JSX.Element }) => {
});
// Check model invalid
useEffect(() => {
if (
userInfo?.username === 'root' &&
(llmModelList.length === 0 || embeddingModelList.length === 0)
) {
toast({
status: 'warning',
title: t('login:model_not_config')
});
router.push('/account/model');
useDebounceEffect(
() => {
if (userInfo?.username === 'root') {
if (llmModelList.length === 0) {
toast({
status: 'warning',
title: t('common:llm_model_not_config')
});
router.push('/account/model');
} else if (embeddingModelList.length === 0) {
toast({
status: 'warning',
title: t('common:embedding_model_not_config')
});
router.push('/account/model');
}
}
},
[embeddingModelList.length, llmModelList.length, userInfo?.username],
{
wait: 2000
}
}, [embeddingModelList.length, llmModelList.length, router, t, toast, userInfo?.username]);
);
return (
<>

View File

@@ -35,34 +35,46 @@ const OneRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
return props.size ? size[props.size] : size['md'];
}, [props.size]);
const avatarList = list.map((item) => {
const modelData = getModelFromList(
[
...llmModelList,
...embeddingModelList,
...ttsModelList,
...sttModelList,
...reRankModelList
],
item.value
);
const avatarList = useMemo(
() =>
list.map((item) => {
const modelData = getModelFromList(
[
...llmModelList,
...embeddingModelList,
...ttsModelList,
...sttModelList,
...reRankModelList
],
item.value
);
return {
value: item.value,
label: (
<Flex alignItems={'center'} py={1}>
<Avatar
borderRadius={'0'}
mr={2}
src={modelData?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={avatarSize}
/>
<Box>{modelData.name}</Box>
</Flex>
)
};
});
return {
value: item.value,
label: (
<Flex alignItems={'center'} py={1}>
<Avatar
borderRadius={'0'}
mr={2}
src={modelData?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={avatarSize}
/>
<Box>{modelData.name}</Box>
</Flex>
)
};
}),
[
list,
llmModelList,
embeddingModelList,
ttsModelList,
sttModelList,
reRankModelList,
avatarSize
]
);
return (
<Box
@@ -99,6 +111,16 @@ const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) =>
const { t } = useTranslation();
const { llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList } =
useSystemStore();
const modelList = useMemo(() => {
return [
...llmModelList,
...embeddingModelList,
...ttsModelList,
...sttModelList,
...reRankModelList
];
}, [llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList]);
const [value, setValue] = useState<string[]>([]);
const avatarSize = useMemo(() => {
@@ -134,7 +156,7 @@ const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) =>
}));
for (const item of list) {
const modelData = getModelFromList([...llmModelList, ...embeddingModelList], item.value);
const modelData = getModelFromList(modelList, item.value);
const provider =
renderList.find((item) => item.value === (modelData?.provider || 'Other')) ??
renderList[renderList.length - 1];
@@ -146,7 +168,7 @@ const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) =>
}
return renderList.filter((item) => item.children.length > 0);
}, [avatarSize, list, llmModelList, t, embeddingModelList]);
}, [avatarSize, list, modelList]);
const onSelect = useCallback(
(e: string[]) => {
@@ -156,16 +178,7 @@ const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) =>
);
const SelectedModel = useMemo(() => {
const modelData = getModelFromList(
[
...llmModelList,
...embeddingModelList,
...ttsModelList,
...sttModelList,
...reRankModelList
],
props.value
);
const modelData = getModelFromList(modelList, props.value);
setValue([modelData.provider, props.value]);
@@ -181,15 +194,7 @@ const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) =>
<Box>{modelData?.name}</Box>
</HStack>
);
}, [
llmModelList,
embeddingModelList,
ttsModelList,
sttModelList,
reRankModelList,
props.value,
avatarSize
]);
}, [modelList, props.value, avatarSize]);
return (
<Box

View File

@@ -8,11 +8,9 @@ import {
ModalBody,
ModalFooter,
Switch,
Textarea,
useTheme
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MySlider from '@/components/Slider';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
@@ -110,12 +108,17 @@ const DatasetParamsModal = ({
useEffect(() => {
if (datasetSearchUsingCfrForm) {
!queryExtensionModel &&
setValue('datasetSearchExtensionModel', chatModelSelectList[0]?.value);
!queryExtensionModel && setValue('datasetSearchExtensionModel', defaultModels.llm?.model);
} else {
setValue('datasetSearchExtensionModel', '');
}
}, [chatModelSelectList, datasetSearchUsingCfrForm, queryExtensionModel, setValue]);
}, [
chatModelSelectList,
datasetSearchUsingCfrForm,
defaultModels.llm?.model,
queryExtensionModel,
setValue
]);
// 保证只有 80 左右个刻度。
const maxTokenStep = useMemo(() => {

View File

@@ -107,7 +107,6 @@ const ChatInput = ({
);
/* whisper init */
const { sttModelList } = useSystemStore();
const canvasRef = useRef<HTMLCanvasElement>(null);
const {
isSpeaking,
@@ -293,7 +292,7 @@ const ChatInput = ({
/>
<Flex alignItems={'center'} position={'absolute'} right={[2, 4]} bottom={['10px', '12px']}>
{/* voice-input */}
{whisperConfig.open && !inputValue && !isChatting && sttModelList.length > 0 && (
{whisperConfig?.open && !inputValue && !isChatting && (
<>
<canvas
ref={canvasRef}
@@ -430,8 +429,7 @@ const ChatInput = ({
speakingTimeString,
stopSpeak,
t,
whisperConfig.open,
sttModelList
whisperConfig?.open
]
);

View File

@@ -13,9 +13,11 @@ import { useRouter } from 'next/router';
const TeamSelector = ({
showManage,
onChange,
...props
}: ButtonProps & {
}: Omit<ButtonProps, 'onChange'> & {
showManage?: boolean;
onChange?: () => void;
}) => {
const { t } = useTranslation();
const router = useRouter();
@@ -36,6 +38,7 @@ const TeamSelector = ({
{
onFinally: () => {
setLoading(false);
onChange?.();
},
errorToast: t('common:user.team.Switch Team Failed')
}

View File

@@ -148,13 +148,12 @@ const OptionItem = ({
mb={4}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
>
<HStack spacing={1}>
<HStack spacing={1} {...provided.dragHandleProps}>
<MyTooltip label={t('common:common.Delete')}>
<MyIcon
mt={0.5}

View File

@@ -142,19 +142,36 @@ const NodeCard = (props: Props) => {
const { runAsync: onClickSyncVersion } = useRequest2(
async () => {
if (!node?.pluginId) return;
const template = await getPreviewPluginNode({ appId: node.pluginId });
if (!node) return;
if (!!template) {
if (node.pluginId) {
const template = await getPreviewPluginNode({ appId: node.pluginId });
if (!!template) {
onResetNode({
id: nodeId,
node: template
});
}
} else {
const template = moduleTemplatesFlat.find(
(item) => item.flowNodeType === node.flowNodeType
);
if (!template) {
return toast({
title: t('app:app.modules.not_found_tips'),
status: 'warning'
});
}
onResetNode({
id: nodeId,
node: template
});
}
onCloseConfirmSync();
},
{
refreshDeps: [node, nodeId, onResetNode]
refreshDeps: [node, nodeId, onResetNode],
onFinally() {}
}
);
@@ -311,7 +328,6 @@ const NodeCard = (props: Props) => {
</Box>
)}
<MenuRender nodeId={nodeId} menuForbid={menuForbid} nodeList={nodeList} />
<ConfirmSyncModal />
</Box>
);
}, [
@@ -335,7 +351,6 @@ const NodeCard = (props: Props) => {
intro,
menuForbid,
nodeList,
ConfirmSyncModal,
onChangeNode,
onOpenCustomTitleModal,
toast

View File

@@ -58,6 +58,7 @@ const RenderOutput = ({
}
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [copyOutputs, nodeId, inputs, llmModelList]);
const [editField, setEditField] = useState<FlowNodeOutputItemType>();
@@ -154,6 +155,7 @@ const RenderOutput = ({
(item) =>
item.type !== FlowNodeOutputTypeEnum.dynamic && item.type !== FlowNodeOutputTypeEnum.hidden
);
return (
<>
{renderOutputs.map((output, i) => {
@@ -163,7 +165,7 @@ const RenderOutput = ({
required={output.required}
position={'relative'}
_notLast={{
mb: 4
mb: i !== renderOutputs.length - 1 ? 4 : 0
}}
>
<OutputLabel nodeId={nodeId} output={output} />

View File

@@ -95,18 +95,15 @@ const TemplateMarketModal = ({
const { runAsync: onUseTemplate, loading: isCreating } = useRequest2(
async (template: AppTemplateSchemaType) => {
const templateDetail = await getTemplateMarketItemDetail(template.templateId);
let workflow = templateDetail.workflow;
if (templateDetail.type === AppTypeEnum.simple) {
workflow = form2AppWorkflow(workflow, t);
}
return postCreateApp({
parentId,
avatar: template.avatar,
name: template.name,
type: template.type as AppTypeEnum,
modules: workflow.nodes || [],
edges: workflow.edges || [],
chatConfig: workflow.chatConfig
modules: templateDetail.workflow.nodes || [],
edges: templateDetail.workflow.edges || [],
chatConfig: templateDetail.workflow.chatConfig
}).then((res) => {
webPushTrack.useAppTemplate({
id: res,

View File

@@ -7,7 +7,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import React, { DragEvent, useCallback, useMemo, useState } from 'react';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { uploadFile2DB } from '@/web/common/file/controller';
@@ -66,9 +66,50 @@ const FileSelector = ({
'i'
);
const { mutate: onSelectFile, isLoading } = useRequest({
mutationFn: async (files: SelectFileItemType[]) => {
const { runAsync: onSelectFile, loading: isLoading } = useRequest2(
async (files: SelectFileItemType[]) => {
{
await Promise.all(
files.map(async ({ fileId, file }) => {
const { fileId: uploadFileId } = await uploadFile2DB({
file,
bucketName: BucketNameEnum.dataset,
data: {
datasetId
},
percentListen: (e) => {
setSelectFiles((state) =>
state.map((item) =>
item.id === fileId
? {
...item,
uploadedFileRate: item.uploadedFileRate
? Math.max(e, item.uploadedFileRate)
: e
}
: item
)
);
}
});
setSelectFiles((state) =>
state.map((item) =>
item.id === fileId
? {
...item,
dbFileId: uploadFileId,
isUploading: false,
uploadedFileRate: 100
}
: item
)
);
})
);
}
},
{
onBefore([files]) {
onStartSelect();
setSelectFiles((state) => {
const formatFiles = files.map<ImportSourceItemType>((selectFile) => {
@@ -88,52 +129,12 @@ const FileSelector = ({
const results = formatFiles.concat(state).slice(0, maxCount);
return results;
});
try {
// upload file
await Promise.all(
files.map(async ({ fileId, file }) => {
const { fileId: uploadFileId } = await uploadFile2DB({
file,
bucketName: BucketNameEnum.dataset,
data: {
datasetId
},
percentListen: (e) => {
setSelectFiles((state) =>
state.map((item) =>
item.id === fileId
? {
...item,
uploadedFileRate: item.uploadedFileRate
? Math.max(e, item.uploadedFileRate)
: e
}
: item
)
);
}
});
setSelectFiles((state) =>
state.map((item) =>
item.id === fileId
? {
...item,
dbFileId: uploadFileId,
isUploading: false,
uploadedFileRate: 100
}
: item
)
);
})
);
} catch (error) {
console.log(error);
}
},
onFinally() {
onFinishSelect();
}
}
});
);
const selectFileCallback = useCallback(
(files: SelectFileItemType[]) => {

View File

@@ -110,7 +110,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
const theme = useTheme();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo, updateUserInfo, teamPlanStatus } = useUserStore();
const { userInfo, updateUserInfo, teamPlanStatus, initUserInfo } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
@@ -284,7 +284,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:user_team_team_name')}:&nbsp;</Box>
<Flex flex={'1 0 0'} w={0} align={'center'}>
<TeamSelector height={'28px'} w={'100%'} showManage />
<TeamSelector height={'28px'} w={'100%'} showManage onChange={initUserInfo} />
</Flex>
</Flex>
)}

View File

@@ -35,7 +35,10 @@ const Team = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { setEditTeamData, isLoading, teamSize } = useContextSelector(TeamContext, (v) => v);
const { setEditTeamData, isLoading, teamSize, refetchMembers } = useContextSelector(
TeamContext,
(v) => v
);
const Tabs = useMemo(
() => (
@@ -85,7 +88,7 @@ const Team = () => {
</Box>
</Flex>
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
<TeamSelector height={'28px'} onChange={refetchMembers} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>

View File

@@ -44,12 +44,22 @@ async function handler(
defaultModels: global.systemDefaultModel
};
} catch (error) {
const referer = req.headers.referer;
if (referer?.includes('/price')) {
return {
feConfigs: global.feConfigs,
subPlans: global.subPlans,
activeModelList
};
}
const unAuthBufferId = global.systemInitBufferId ? `unAuth_${global.systemInitBufferId}` : '';
if (bufferId && unAuthBufferId === bufferId) {
return {
bufferId: unAuthBufferId
};
}
return {
bufferId: unAuthBufferId,
feConfigs: global.feConfigs

View File

@@ -197,7 +197,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
return {
...app,
permission: Per,
privateApp
private: privateApp
};
})
.filter((app) => app.permission.hasReadPer);

View File

@@ -5,7 +5,7 @@ import {
SseResponseEventEnum
} from '@fastgpt/global/core/workflow/runtime/constants';
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
@@ -244,7 +244,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
}
pushChatUsage({
createChatUsage({
appName,
appId,
teamId,

View File

@@ -177,7 +177,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
tmbId: dataset.tmbId,
updateTime: dataset.updateTime,
permission: Per,
privateDataset
private: privateDataset
};
})
.filter((app) => app.permission.hasReadPer);

View File

@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
@@ -66,8 +66,8 @@ async function handler(
maxRunTimes: WORKFLOW_MAX_RUN_TIMES
});
pushChatUsage({
appName: '工作流Debug',
createChatUsage({
appName: `${app.name}-Debug`,
appId,
teamId,
tmbId,

View File

@@ -20,7 +20,7 @@ import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';
import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools';
import requestIp from 'request-ip';
@@ -423,7 +423,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
// add record
const { totalPoints } = pushChatUsage({
const { totalPoints } = createChatUsage({
appName: app.name,
appId: app._id,
teamId,

View File

@@ -0,0 +1,145 @@
import '@/pages/api/__mocks__/base';
import { parseReasoningStreamContent } from '@fastgpt/global/core/workflow/runtime/utils';
test('Parse reasoning stream content test', async () => {
const partList = [
{
data: [{ content: '你好1' }, { content: '你好2' }, { content: '你好3' }],
correct: { answer: '你好1你好2你好3', reasoning: '' }
},
{
data: [
{ reasoning_content: '这是' },
{ reasoning_content: '思考' },
{ reasoning_content: '过程' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<t' },
{ content: 'hink>' },
{ content: '这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>' },
{ content: '这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</' },
{ content: 'think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</think>你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</th' },
{ content: '假的' },
{ content: '你好2' },
{ content: '你好3' },
{ content: '过程</think>你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程</th假的你好2你好3过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</th' },
{ content: '假的' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '', reasoning: '这是思考过程</th假的你好2你好3' }
}
];
partList.forEach((part) => {
const { parsePart } = parseReasoningStreamContent();
let answer = '';
let reasoning = '';
part.data.forEach((item) => {
const formatPart = {
choices: [
{
delta: {
role: 'assistant',
content: item.content,
reasoning_content: item.reasoning_content
}
}
]
};
const [reasoningContent, content] = parsePart(formatPart, true);
answer += content;
reasoning += reasoningContent;
});
expect(answer).toBe(part.correct.answer);
expect(reasoning).toBe(part.correct.reasoning);
});
});

View File

@@ -67,9 +67,7 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => {
// 检查是否是当前的 route
const navigateTo =
decodeLastRoute && !decodeLastRoute.includes('/login') ? decodeLastRoute : '/app/list';
setTimeout(() => {
router.push(navigateTo);
}, 300);
router.push(navigateTo);
},
[setUserInfo, lastRoute, router]
);

View File

@@ -97,7 +97,7 @@ const provider = () => {
await clearToken();
router.prefetch('/app/list');
if (loginStore && state !== loginStore.state) {
if (loginStore && loginStore.provider !== 'sso' && state !== loginStore.state) {
toast({
status: 'warning',
title: t('common:support.user.login.security_failed')

View File

@@ -1,5 +1,5 @@
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { delay, retryFn } from '@fastgpt/global/common/system/utils';
@@ -113,7 +113,7 @@ export const getScheduleTriggerApp = async () => {
}
]
});
pushChatUsage({
createChatUsage({
appName: app.name,
appId: app._id,
teamId: String(app.teamId),

View File

@@ -1,27 +0,0 @@
import { ConcatUsageProps, CreateUsageProps } from '@fastgpt/global/support/wallet/usage/api';
import { addLog } from '@fastgpt/service/common/system/log';
import { POST } from '@fastgpt/service/common/api/plusRequest';
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
export async function createUsage(data: CreateUsageProps) {
if (!FastGPTProUrl) return;
if (data.totalPoints === 0) {
addLog.info('0 totalPoints', data);
}
try {
await POST('/support/wallet/usage/createUsage', data);
} catch (error) {
addLog.error('createUsage error', error);
}
}
export async function concatUsage(data: ConcatUsageProps) {
if (!FastGPTProUrl) return;
if (data.totalPoints === 0) {
addLog.info('0 totalPoints', data);
}
try {
await POST('/support/wallet/usage/concatUsage', data);
} catch (error) {
addLog.error('concatUsage error', error);
}
}

View File

@@ -1,55 +1,10 @@
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { addLog } from '@fastgpt/service/common/system/log';
import { createUsage, concatUsage } from './controller';
import { createUsage, concatUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { formatModelChars2Points } from '@fastgpt/service/support/wallet/usage/utils';
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { i18nT } from '@fastgpt/web/i18n/utils';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { getDefaultTTSModel } from '@fastgpt/service/core/ai/model';
export const pushChatUsage = ({
appName,
appId,
pluginId,
teamId,
tmbId,
source,
flowUsages
}: {
appName: string;
appId?: string;
pluginId?: string;
teamId: string;
tmbId: string;
source: UsageSourceEnum;
flowUsages: ChatNodeUsageType[];
}) => {
const totalPoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0);
createUsage({
teamId,
tmbId,
appName,
appId,
pluginId,
totalPoints,
source,
list: flowUsages.map((item) => ({
moduleName: item.moduleName,
amount: item.totalPoints || 0,
model: item.model,
inputTokens: item.inputTokens,
outputTokens: item.outputTokens
}))
});
addLog.info(`finish completions`, {
source,
teamId,
totalPoints
});
return { totalPoints };
};
export const pushQAUsage = async ({
teamId,
tmbId,

View File

@@ -24,7 +24,11 @@ export type StreamResponseType = {
[DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[];
};
type ResponseQueueItemType =
| { event: SseResponseEventEnum.fastAnswer | SseResponseEventEnum.answer; text: string }
| {
event: SseResponseEventEnum.fastAnswer | SseResponseEventEnum.answer;
text?: string;
reasoningText?: string;
}
| { event: SseResponseEventEnum.interactive; [key: string]: any }
| {
event:
@@ -79,7 +83,7 @@ export const streamFetch = ({
if (abortCtrl.signal.aborted) {
responseQueue.forEach((item) => {
onMessage(item);
if (isAnswerEvent(item.event)) {
if (isAnswerEvent(item.event) && item.text) {
responseText += item.text;
}
});
@@ -91,7 +95,7 @@ export const streamFetch = ({
for (let i = 0; i < fetchCount; i++) {
const item = responseQueue[i];
onMessage(item);
if (isAnswerEvent(item.event)) {
if (isAnswerEvent(item.event) && item.text) {
responseText += item.text;
}
}
@@ -180,7 +184,7 @@ export const streamFetch = ({
// console.log(parseJson, event);
if (event === SseResponseEventEnum.answer) {
const reasoningText = parseJson.choices?.[0]?.delta?.reasoning_content || '';
onMessage({
pushDataToQueue({
event,
reasoningText
});
@@ -194,7 +198,7 @@ export const streamFetch = ({
}
} else if (event === SseResponseEventEnum.fastAnswer) {
const reasoningText = parseJson.choices?.[0]?.delta?.reasoning_content || '';
onMessage({
pushDataToQueue({
event,
reasoningText
});

View File

@@ -31,7 +31,7 @@ export const uploadFile2DB = ({
if (!e.total) return;
const percent = Math.round((e.loaded / e.total) * 100);
percentListen && percentListen(percent);
percentListen?.(percent);
});
};

View File

@@ -1,5 +1,6 @@
import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { useSystemStore } from './useSystemStore';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
export const downloadFetch = async ({
url,
@@ -12,7 +13,7 @@ export const downloadFetch = async ({
}) => {
if (body) {
// fetch data with POST method if body exists
const response = await fetch(url, {
const response = await fetch(getWebReqUrl(url), {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -78,7 +78,7 @@ export const useInitApp = () => {
if (sourceDomain) return sourceDomain;
return document.referrer;
})();
console.log(formatSourceDomain, '-=-=');
if (formatSourceDomain && !sessionStorage.getItem('sourceDomain')) {
sessionStorage.setItem('sourceDomain', formatSourceDomain);
}