Compare commits

..

1 Commits

Author SHA1 Message Date
gru-agent[bot]
74be2169e4 Add unit test for projects/app/src/web/core/chat/context/useChatStore.ts 2025-05-14 07:44:15 +00:00
100 changed files with 1044 additions and 1252 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

View File

@@ -1,26 +0,0 @@
---
title: 'V4.9.9(进行中)'
description: 'FastGPT V4.9.9 更新说明'
icon: 'upgrade'
draft: false
toc: true
weight: 791
---
## 🚀 新增内容
1. 切换 SessionId 来替代 JWT 实现登录鉴权,可控制最大登录客户端数量。
2. 新的商业版 License 管理模式。
3. 公众号调用,显示记录 chat 对话错误,方便排查。
## ⚙️ 优化
1. 优化工具调用,新工具的判断逻辑。
2. 调整 Cite 引用提示词。
## 🐛 修复
1. 无法正常获取应用历史保存/发布记录。
2. 成员创建 MCP 工具权限问题。
3. 来源引用展示,存在 ID 传递错误,导致提示无权操作该文件。

View File

@@ -185,40 +185,3 @@ curl --location --request GET '{{baseURL}}/v1/file/read?id=xx' \
{{< /tabs >}}
### 4. 获取文件详细信息(用于获取文件信息)
{{< tabs tabTotal="2" >}}
{{< tab tabName="请求示例" >}}
{{< markdownify >}}
id 为文件的 id。
```bash
curl --location --request GET '{{baseURL}}/v1/file/detail?id=xx' \
--header 'Authorization: Bearer {{authorization}}'
```
{{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="响应示例" >}}
{{< markdownify >}}
```json
{
"code": 200,
"success": true,
"message": "",
"data": {
"id": "docs",
"parentId": "",
"name": "docs"
}
}
```
{{< /markdownify >}}
{{< /tab >}}
{{< /tabs >}}

View File

@@ -132,9 +132,7 @@ weight: 506
### 公众号没响应
检查应用对话日志,如果有对话日志,但是微信公众号无响应,则是白名单 IP未成功。
添加白名单IP 后,通常需要等待几分钟微信更新。可以在对话日志中,找点错误日志。
![](/imgs/official_account_faq.png)
添加白名单IP 后,通常需要等待几分钟微信更新。
### 如何新开一个聊天记录

2
env.d.ts vendored
View File

@@ -4,6 +4,7 @@ declare global {
LOG_DEPTH: string;
DEFAULT_ROOT_PSW: string;
DB_MAX_LINK: string;
TOKEN_KEY: string;
FILE_TOKEN_KEY: string;
ROOT_KEY: string;
OPENAI_BASE_URL: string;
@@ -36,7 +37,6 @@ declare global {
CONFIG_JSON_PATH?: string;
PASSWORD_LOGIN_LOCK_SECONDS?: string;
PASSWORD_EXPIRED_MONTH?: string;
MAX_LOGIN_SESSION?: string;
}
}
}

View File

@@ -2,248 +2,6 @@ import { type PromptTemplateItem } from '../type.d';
import { i18nT } from '../../../../web/i18n/utils';
import { getPromptByVersion } from './utils';
export const Prompt_userQuotePromptList: PromptTemplateItem[] = [
{
title: i18nT('app:template.standard_template'),
desc: '',
value: {
['4.9.7']: `## 任务描述
你是一个知识库回答助手,可以使用 <Cites></Cites> 中的内容作为你本次回答的参考。
同时,为了使回答结果更加可信并且可追溯,你需要在每段话结尾添加引用标记,标识参考了哪些内容。
## 追溯展示规则
- 使用 [id](CITE) 的格式来引用 <Cites></Cites> 中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在 **每段话结尾** 自然地整合引用。例如: "Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)。"。
- 每段话**至少包含一个引用**,多个引用时按顺序排列,例如:"Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6862](CITE)。\n 它的特点是非常轻量[67e517e74767063e882d6863](CITE)。"
- 不要把示例作为知识点。
- 不要伪造 id返回的 id 必须都存在 <Cites></Cites> 中!
## 通用规则
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Cites></Cites> 获取的知识。
- 保持答案与 <Cites></Cites> 中描述的一致。
- 使用 Markdown 语法优化回答格式。尤其是图片、表格、序列号等内容,需严格完整输出。
- 使用与问题相同的语言回答。
<Cites>
{{quote}}
</Cites>
## 用户问题
{{question}}
## 回答
`
}
},
{
title: i18nT('app:template.qa_template'),
desc: '',
value: {
['4.9.7']: `## 任务描述
作为一个问答助手,你会使用 <QA></QA> 标记中的提供的数据对进行内容回答。
## 回答要求
- 选择其中一个或多个问答对进行回答。
- 回答的内容应尽可能与 <Answer></Answer> 中的内容一致。
- 如果没有相关的问答对,你需要澄清。
- 避免提及你是从 <QA></QA> 获取的知识,只需要回复答案。
- 使用与问题相同的语言回答。
<QA>
{{quote}}
</QA>
## 用户问题
{{question}}
## 回答
`
}
},
{
title: i18nT('app:template.standard_strict'),
desc: '',
value: {
['4.9.7']: `## 任务描述
你是一个知识库回答助手,可以使用 <Cites></Cites> 中的内容作为你本次回答的参考。
同时,为了使回答结果更加可信并且可追溯,你需要在每段话结尾添加引用标记,标识参考了哪些内容。
## 追溯展示规则
- 使用 [id](CITE) 的格式来引用 <Cites></Cites> 中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在 **每段话结尾** 自然地整合引用。例如: "Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)。"。
- 每段话**至少包含一个引用**,多个引用时按顺序排列,例如:"Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6862](CITE)。\n 它的特点是非常轻量[67e517e74767063e882d6863](CITE)。"
- 不要把示例作为知识点。
- 不要伪造 id返回的 id 必须都存在 <Cites></Cites> 中!
## 通用规则
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Cites></Cites> 获取的知识。
- 保持答案与 <Cites></Cites> 中描述的一致。
- 使用 Markdown 语法优化回答格式。尤其是图片、表格、序列号等内容,需严格完整输出。
- 使用与问题相同的语言回答。
## 严格要求
你只能使用 <Cites></Cites> 标记中的内容作为参考,不能使用自身的知识,并且回答的内容需严格与 <Cites></Cites> 中的内容一致。
<Cites>
{{quote}}
</Cites>
## 用户问题
{{question}}
## 回答
`
}
},
{
title: i18nT('app:template.hard_strict'),
desc: '',
value: {
['4.9.7']: `## 任务描述
作为一个问答助手,你会使用 <QA></QA> 标记中的提供的数据对进行内容回答。
## 回答要求
- 选择其中一个或多个问答对进行回答。
- 回答的内容应尽可能与 <Answer></Answer> 中的内容一致。
- 如果没有相关的问答对,你需要澄清。
- 避免提及你是从 <QA></QA> 获取的知识,只需要回复答案。
- 使用与问题相同的语言回答。
## 严格要求
你只能使用 <QA></QA> 标记中的内容作为参考,不能使用自身的知识,并且回答的内容需严格与 <QA></QA> 中的内容一致。
<QA>
{{quote}}
</QA>
## 用户问题
{{question}}
## 回答
`
}
}
];
export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [
{
title: i18nT('app:template.standard_template'),
desc: '',
value: {
['4.9.7']: `## 任务描述
你是一个知识库回答助手,可以使用 <Cites></Cites> 中的内容作为你本次回答的参考。
同时,为了使回答结果更加可信并且可追溯,你需要在每段话结尾添加引用标记,标识参考了哪些内容。
## 追溯展示规则
- 使用 [id](CITE) 的格式来引用 <Cites></Cites> 中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在 **每段话结尾** 自然地整合引用。例如: "Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)。"。
- 每段话**至少包含一个引用**,多个引用时按顺序排列,例如:"Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6862](CITE)。\n 它的特点是非常轻量[67e517e74767063e882d6863](CITE)。"
- 不要把示例作为知识点。
- 不要伪造 id返回的 id 必须都存在 <Cites></Cites> 中!
## 通用规则
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Cites></Cites> 获取的知识。
- 保持答案与 <Cites></Cites> 中描述的一致。
- 使用 Markdown 语法优化回答格式。尤其是图片、表格、序列号等内容,需严格完整输出。
- 使用与问题相同的语言回答。
<Cites>
{{quote}}
</Cites>`
}
},
{
title: i18nT('app:template.qa_template'),
desc: '',
value: {
['4.9.8']: `## 任务描述
作为一个问答助手,你会使用 <QA></QA> 标记中的提供的数据对进行内容回答。
## 回答要求
- 选择其中一个或多个问答对进行回答。
- 回答的内容应尽可能与 <Answer></Answer> 中的内容一致。
- 如果没有相关的问答对,你需要澄清。
- 避免提及你是从 <QA></QA> 获取的知识,只需要回复答案。
- 使用与问题相同的语言回答。
<QA>
{{quote}}
</QA>`
}
},
{
title: i18nT('app:template.standard_strict'),
desc: '',
value: {
['4.9.7']: `## 任务描述
你是一个知识库回答助手,可以使用 <Cites></Cites> 中的内容作为你本次回答的参考。
同时,为了使回答结果更加可信并且可追溯,你需要在每段话结尾添加引用标记,标识参考了哪些内容。
## 追溯展示规则
- 使用 [id](CITE) 的格式来引用 <Cites></Cites> 中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在 **每段话结尾** 自然地整合引用。例如: "Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)。"。
- 每段话**至少包含一个引用**,多个引用时按顺序排列,例如:"Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6862](CITE)。\n 它的特点是非常轻量[67e517e74767063e882d6863](CITE)。"
- 不要把示例作为知识点。
- 不要伪造 id返回的 id 必须都存在 <Cites></Cites> 中!
## 通用规则
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Cites></Cites> 获取的知识。
- 保持答案与 <Cites></Cites> 中描述的一致。
- 使用 Markdown 语法优化回答格式。尤其是图片、表格、序列号等内容,需严格完整输出。
- 使用与问题相同的语言回答。
## 严格要求
你只能使用 <Cites></Cites> 标记中的内容作为参考,不能使用自身的知识,并且回答的内容需严格与 <Cites></Cites> 中的内容一致。
<Cites>
{{quote}}
</Cites>`
}
},
{
title: i18nT('app:template.hard_strict'),
desc: '',
value: {
['4.9.7']: `## 任务描述
作为一个问答助手,你会使用 <QA></QA> 标记中的提供的数据对进行内容回答。
## 回答要求
- 选择其中一个或多个问答对进行回答。
- 回答的内容应尽可能与 <Answer></Answer> 中的内容一致。
- 如果没有相关的问答对,你需要澄清。
- 避免提及你是从 <QA></QA> 获取的知识,只需要回复答案。
- 使用与问题相同的语言回答。
## 严格要求
你只能使用 <QA></QA> 标记中的内容作为参考,不能使用自身的知识,并且回答的内容需严格与 <QA></QA> 中的内容一致。
<QA>
{{quote}}
</QA>`
}
}
];
export const Prompt_QuoteTemplateList: PromptTemplateItem[] = [
{
title: i18nT('app:template.standard_template'),
@@ -252,6 +10,11 @@ export const Prompt_QuoteTemplateList: PromptTemplateItem[] = [
['4.9.7']: `{
"id": "{{id}}",
"sourceName": "{{source}}",
"content": "{{q}}\n{{a}}"
}
`,
['4.9.2']: `{
"sourceName": "{{source}}",
"updateTime": "{{updateTime}}",
"content": "{{q}}\n{{a}}"
}
@@ -262,7 +25,7 @@ export const Prompt_QuoteTemplateList: PromptTemplateItem[] = [
title: i18nT('app:template.qa_template'),
desc: i18nT('app:template.qa_template_des'),
value: {
['4.9.7']: `<Question>
['4.9.2']: `<Question>
{{q}}
</Question>
<Answer>
@@ -277,6 +40,11 @@ export const Prompt_QuoteTemplateList: PromptTemplateItem[] = [
['4.9.7']: `{
"id": "{{id}}",
"sourceName": "{{source}}",
"content": "{{q}}\n{{a}}"
}
`,
['4.9.2']: `{
"sourceName": "{{source}}",
"updateTime": "{{updateTime}}",
"content": "{{q}}\n{{a}}"
}
@@ -287,7 +55,7 @@ export const Prompt_QuoteTemplateList: PromptTemplateItem[] = [
title: i18nT('app:template.hard_strict'),
desc: i18nT('app:template.hard_strict_des'),
value: {
['4.9.7']: `<Question>
['4.9.2']: `<Question>
{{q}}
</Question>
<Answer>
@@ -296,12 +64,263 @@ export const Prompt_QuoteTemplateList: PromptTemplateItem[] = [
}
}
];
export const getQuoteTemplate = (version?: string) => {
const defaultTemplate = Prompt_QuoteTemplateList[0].value;
return getPromptByVersion(version, defaultTemplate);
};
export const Prompt_userQuotePromptList: PromptTemplateItem[] = [
{
title: i18nT('app:template.standard_template'),
desc: '',
value: {
['4.9.7']: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
回答要求:
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。
- 使用 [id](CITE) 格式来引用<Reference></Reference>中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。"
- 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。`,
['4.9.2']: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
回答要求:
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。
问题:"""{{question}}"""`
}
},
{
title: i18nT('app:template.qa_template'),
desc: '',
value: {
['4.9.2']: `使用 <QA></QA> 标记中的问答对进行回答。
<QA>
{{quote}}
</QA>
回答要求:
- 选择其中一个或多个问答对进行回答。
- 回答的内容应尽可能与 <答案></答案> 中的内容一致。
- 如果没有相关的问答对,你需要澄清。
- 避免提及你是从 QA 获取的知识,只需要回复答案。
问题:"""{{question}}"""`
}
},
{
title: i18nT('app:template.standard_strict'),
desc: '',
value: {
['4.9.7']: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
思考流程:
1. 判断问题是否与 <Reference></Reference> 标记中的内容有关。
2. 如果有关,你按下面的要求回答。
3. 如果无关,你直接拒绝回答本次问题。
回答要求:
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。
- 使用 [id](CITE) 格式来引用<Reference></Reference>中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。"
- 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。
问题:"""{{question}}"""`,
['4.9.2']: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
思考流程:
1. 判断问题是否与 <Reference></Reference> 标记中的内容有关。
2. 如果有关,你按下面的要求回答。
3. 如果无关,你直接拒绝回答本次问题。
回答要求:
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。
问题:"""{{question}}"""`
}
},
{
title: i18nT('app:template.hard_strict'),
desc: '',
value: {
['4.9.2']: `忘记你已有的知识,仅使用 <QA></QA> 标记中的问答对进行回答。
<QA>
{{quote}}
</QA>
思考流程:
1. 判断问题是否与 <QA></QA> 标记中的内容有关。
2. 如果无关,你直接拒绝回答本次问题。
3. 判断是否有相近或相同的问题。
4. 如果有相同的问题,直接输出对应答案。
5. 如果只有相近的问题,请把相近的问题和答案一起输出。
回答要求:
- 如果没有相关的问答对,你需要澄清。
- 回答的内容应尽可能与 <QA></QA> 标记中的内容一致。
- 避免提及你是从 QA 获取的知识,只需要回复答案。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。
问题:"""{{question}}"""`
}
}
];
export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [
{
title: i18nT('app:template.standard_template'),
desc: '',
value: {
['4.9.7']: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
回答要求:
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。
- 使用 [id](CITE) 格式来引用<Reference></Reference>中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。"
- 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。`,
['4.9.2']: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
回答要求:
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。`
}
},
{
title: i18nT('app:template.qa_template'),
desc: '',
value: {
['4.9.2']: `使用 <QA></QA> 标记中的问答对进行回答。
<QA>
{{quote}}
</QA>
回答要求:
- 选择其中一个或多个问答对进行回答。
- 回答的内容应尽可能与 <答案></答案> 中的内容一致。
- 如果没有相关的问答对,你需要澄清。
- 避免提及你是从 QA 获取的知识,只需要回复答案。`
}
},
{
title: i18nT('app:template.standard_strict'),
desc: '',
value: {
['4.9.7']: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
思考流程:
1. 判断问题是否与 <Reference></Reference> 标记中的内容有关。
2. 如果有关,你按下面的要求回答。
3. 如果无关,你直接拒绝回答本次问题。
回答要求:
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。
- 使用 [id](CITE) 格式来引用<Reference></Reference>中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在每段结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。"
- 每段至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。
问题:"""{{question}}"""`,
['4.9.2']: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference>
{{quote}}
</Reference>
思考流程:
1. 判断问题是否与 <Reference></Reference> 标记中的内容有关。
2. 如果有关,你按下面的要求回答。
3. 如果无关,你直接拒绝回答本次问题。
回答要求:
- 避免提及你是从 <Reference></Reference> 获取的知识。
- 保持答案与 <Reference></Reference> 中描述的一致。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。`
}
},
{
title: i18nT('app:template.hard_strict'),
desc: '',
value: {
['4.9.2']: `忘记你已有的知识,仅使用 <QA></QA> 标记中的问答对进行回答。
<QA>
{{quote}}
</QA>
思考流程:
1. 判断问题是否与 <QA></QA> 标记中的内容有关。
2. 如果无关,你直接拒绝回答本次问题。
3. 判断是否有相近或相同的问题。
4. 如果有相同的问题,直接输出对应答案。
5. 如果只有相近的问题,请把相近的问题和答案一起输出。
回答要求:
- 如果没有相关的问答对,你需要澄清。
- 回答的内容应尽可能与 <QA></QA> 标记中的内容一致。
- 避免提及你是从 QA 获取的知识,只需要回复答案。
- 使用 Markdown 语法优化回答格式。
- 使用与问题相同的语言回答。`
}
}
];
export const getQuotePrompt = (version?: string, role: 'user' | 'system' = 'user') => {
const quotePromptTemplates =
role === 'user' ? Prompt_userQuotePromptList : Prompt_systemQuotePromptList;
@@ -314,7 +333,7 @@ export const getQuotePrompt = (version?: string, role: 'user' | 'system' = 'user
// Document quote prompt
export const getDocumentQuotePrompt = (version?: string) => {
const promptMap = {
['4.9.7']: `将 <FilesContent></FilesContent> 中的内容作为本次对话的参考:
['4.9.2']: `将 <FilesContent></FilesContent> 中的内容作为本次对话的参考:
<FilesContent>
{{quote}}
</FilesContent>

View File

@@ -1,19 +1,14 @@
export const getDatasetSearchToolResponsePrompt = () => {
return `## Role
你是一个知识库回答助手,可以 "cites" 中的内容作为本次对话的参考。为了使回答结果更加可信并且可追溯,你需要在每段话结尾添加引用标记,标识参考了哪些内容
你是一个知识库回答助手,可以 "quotes" 中的内容作为本次对话的参考。为了使回答结果更加可信并且可追溯,你需要在每段话结尾添加引用标记。
## 追溯展示规则
- 使用 **[id](CITE)** 格式来引用 "cites" 中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在 **每段话结尾** 自然地整合引用。例如: "Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)。"。
- 每段话**至少包含一个引用**,多个引用时按顺序排列,例如:"Nginx是一款轻量级的Web服务器、反向代理服务器[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6862](CITE)。\n 它的特点是非常轻量[67e517e74767063e882d6863](CITE)。"
- 不要把示例作为知识点。
- 不要伪造 id返回的 id 必须都存在 cites 中!
## 通用规则
## Rules
- 如果你不清楚答案,你需要澄清。
- 避免提及你是从 "cites" 获取的知识。
- 保持答案与 "cites" 中描述的一致。
- 避免提及你是从 "quotes" 获取的知识。
- 保持答案与 "quotes" 中描述的一致。
- 使用 Markdown 语法优化回答格式。尤其是图片、表格、序列号等内容,需严格完整输出。
- 使用与问题相同的语言回答。`;
- 使用与问题相同的语言回答。
- 使用 [id](CITE) 格式来引用 "quotes" 中的知识,其中 CITE 是固定常量, id 为引文中的 id。
- 在每段话结尾自然地整合引用。例如: "FastGPT 是一个基于大语言模型(LLM)的知识库问答系统[67e517e74767063e882d6861](CITE)。"
- 每段话至少包含一个引用,也可根据内容需要加入多个引用,按顺序排列。`;
};

View File

@@ -5,7 +5,7 @@ import {
FlowNodeTypeEnum
} from '../../workflow/node/constant';
import { nanoid } from 'nanoid';
import { type McpToolConfigType } from '../type';
import { type ToolType } from '../type';
import { i18nT } from '../../../../web/i18n/utils';
import { type RuntimeNodeItemType } from '../../workflow/runtime/type';
@@ -16,7 +16,7 @@ export const getMCPToolSetRuntimeNode = ({
avatar
}: {
url: string;
toolList: McpToolConfigType[];
toolList: ToolType[];
name?: string;
avatar?: string;
}): RuntimeNodeItemType => {
@@ -45,7 +45,7 @@ export const getMCPToolRuntimeNode = ({
url,
avatar = 'core/app/type/mcpToolsFill'
}: {
tool: McpToolConfigType;
tool: ToolType;
url: string;
avatar?: string;
}): RuntimeNodeItemType => {
@@ -65,7 +65,7 @@ export const getMCPToolRuntimeNode = ({
...Object.entries(tool.inputSchema?.properties || {}).map(([key, value]) => ({
key,
label: key,
valueType: value.type as WorkflowIOValueTypeEnum, // TODO: 这里需要做一个映射
valueType: value.type as WorkflowIOValueTypeEnum,
description: value.description,
toolDescription: value.description || key,
required: tool.inputSchema?.required?.includes(key) || false,

View File

@@ -16,6 +16,16 @@ import { FlowNodeInputTypeEnum } from '../../core/workflow/node/constant';
import type { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type';
import type { SourceMemberType } from '../../support/user/type';
export type ToolType = {
name: string;
description: string;
inputSchema: {
type: string;
properties?: Record<string, { type: string; description?: string }>;
required?: string[];
};
};
export type AppSchema = {
_id: string;
parentId?: ParentIdType;
@@ -107,16 +117,6 @@ export type AppSimpleEditFormType = {
chatConfig: AppChatConfigType;
};
export type McpToolConfigType = {
name: string;
description: string;
inputSchema: {
type: string;
properties?: Record<string, { type: string; description?: string }>;
required?: string[];
};
};
/* app chat config type */
export type AppChatConfigType = {
welcomeText?: string;

View File

@@ -9,7 +9,6 @@ import { type WorkflowTemplateBasicType } from '../workflow/type';
import { AppTypeEnum } from './constants';
import { AppErrEnum } from '../../common/error/code/app';
import { PluginErrEnum } from '../../common/error/code/plugin';
import { i18nT } from '../../../web/i18n/utils';
export const getDefaultAppForm = (): AppSimpleEditFormType => {
return {
@@ -190,7 +189,7 @@ export const getAppType = (config?: WorkflowTemplateBasicType | AppSimpleEditFor
return '';
};
export const formatToolError = (error?: string) => {
export const checkAppUnExistError = (error?: string) => {
const unExistError: Array<string> = [
AppErrEnum.unAuthApp,
AppErrEnum.unExist,
@@ -198,9 +197,9 @@ export const formatToolError = (error?: string) => {
PluginErrEnum.unExist
];
if (error && unExistError.includes(error)) {
return i18nT('app:un_auth');
} else {
if (!!error && unExistError.includes(error)) {
return error;
} else {
return undefined;
}
};

View File

@@ -112,7 +112,6 @@ export type ChatItemSchema = (UserChatItemType | SystemChatItemType | AIChatItem
appId: string;
time: Date;
durationSeconds?: number;
errorMsg?: string;
};
export type AdminFbkType = {
@@ -144,7 +143,6 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt
responseData?: ChatHistoryItemResType[];
time?: Date;
durationSeconds?: number;
errorMsg?: string;
} & ChatBoxInputType &
ResponseTagItemType;

View File

@@ -7,7 +7,7 @@ import type {
} from '../../chat/type';
import { NodeOutputItemType } from '../../chat/type';
import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '../type/io.d';
import type { NodeToolConfigType, StoreNodeItemType } from '../type/node';
import type { StoreNodeItemType } from '../type/node';
import type { DispatchNodeResponseKeyEnum } from './constants';
import type { StoreEdgeItemType } from '../type/edge';
import type { NodeInputKeyEnum } from '../constants';
@@ -102,9 +102,6 @@ export type RuntimeNodeItemType = {
pluginId?: string; // workflow id / plugin id
version?: string;
// tool
toolConfig?: NodeToolConfigType;
};
export type RuntimeEdgeItemType = StoreEdgeItemType & {
@@ -117,7 +114,7 @@ export type DispatchNodeResponseType = {
runningTime?: number;
query?: string;
textOutput?: string;
error?: Record<string, any> | string;
error?: Record<string, any>;
customInputs?: Record<string, any>;
customOutputs?: Record<string, any>;
nodeInputs?: Record<string, any>;

View File

@@ -20,17 +20,11 @@ import { RuntimeNodeItemType } from '../runtime/type';
import { PluginTypeEnum } from '../../plugin/constants';
import { RuntimeEdgeItemType, StoreEdgeItemType } from './edge';
import { NextApiResponse } from 'next';
import type { AppDetailType, AppSchema, McpToolConfigType } from '../../app/type';
import { AppDetailType, AppSchema } from '../../app/type';
import type { ParentIdType } from 'common/parentFolder/type';
import { AppTypeEnum } from '../../app/constants';
import { AppTypeEnum } from 'core/app/constants';
import type { WorkflowInteractiveResponseType } from '../template/system/interactive/type';
export type NodeToolConfigType = {
mcpTool?: McpToolConfigType & {
url: string;
};
};
export type FlowNodeCommonType = {
parentNodeId?: string;
flowNodeType: FlowNodeTypeEnum; // render node card
@@ -52,10 +46,8 @@ export type FlowNodeCommonType = {
// plugin data
pluginId?: string;
isFolder?: boolean;
// pluginType?: AppTypeEnum;
pluginData?: PluginDataType;
// tool data
toolData?: NodeToolConfigType;
};
export type PluginDataType = {

View File

@@ -149,7 +149,7 @@ export const readRawContentByFileBuffer = async ({
return await systemParse();
})();
addLog.debug(`Parse file success, time: ${Date.now() - start}ms. `);
addLog.debug(`Parse file success, time: ${Date.now() - start}ms. Uploading file image.`);
// markdown data format
if (imageList) {
@@ -185,7 +185,7 @@ export const readRawContentByFileBuffer = async ({
}
}
addLog.debug(`Upload file success, time: ${Date.now() - start}ms`);
addLog.debug(`Upload file image success, time: ${Date.now() - start}ms`);
return { rawText, formatText, imageList };
};

View File

@@ -1,10 +1,7 @@
import { getGlobalRedisConnection } from './index';
import { getGlobalRedisCacheConnection } from './index';
import { addLog } from '../system/log';
import { retryFn } from '@fastgpt/global/common/system/utils';
const redisPrefix = 'cache:';
const getCacheKey = (key: string) => `${redisPrefix}${key}`;
export enum CacheKeyEnum {
team_vector_count = 'team_vector_count'
}
@@ -16,12 +13,12 @@ export const setRedisCache = async (
) => {
return await retryFn(async () => {
try {
const redis = getGlobalRedisConnection();
const redis = getGlobalRedisCacheConnection();
if (expireSeconds) {
await redis.set(getCacheKey(key), data, 'EX', expireSeconds);
await redis.set(key, data, 'EX', expireSeconds);
} else {
await redis.set(getCacheKey(key), data);
await redis.set(key, data);
}
} catch (error) {
addLog.error('Set cache error:', error);
@@ -31,11 +28,11 @@ export const setRedisCache = async (
};
export const getRedisCache = async (key: string) => {
const redis = getGlobalRedisConnection();
return await retryFn(() => redis.get(getCacheKey(key)));
const redis = getGlobalRedisCacheConnection();
return await retryFn(() => redis.get(key));
};
export const delRedisCache = async (key: string) => {
const redis = getGlobalRedisConnection();
await retryFn(() => redis.del(getCacheKey(key)));
const redis = getGlobalRedisCacheConnection();
await retryFn(() => redis.del(key));
};

View File

@@ -27,26 +27,17 @@ export const newWorkerRedisConnection = () => {
return redis;
};
export const FASTGPT_REDIS_PREFIX = 'fastgpt:';
export const getGlobalRedisConnection = () => {
if (global.redisClient) return global.redisClient;
export const getGlobalRedisCacheConnection = () => {
if (global.redisCache) return global.redisCache;
global.redisClient = new Redis(REDIS_URL, { keyPrefix: FASTGPT_REDIS_PREFIX });
global.redisCache = new Redis(REDIS_URL, { keyPrefix: 'fastgpt:cache:' });
global.redisClient.on('connect', () => {
global.redisCache.on('connect', () => {
addLog.info('Redis connected');
});
global.redisClient.on('error', (error) => {
global.redisCache.on('error', (error) => {
addLog.error('Redis connection error', error);
});
return global.redisClient;
};
export const getAllKeysByPrefix = async (key: string) => {
const redis = getGlobalRedisConnection();
const keys = (await redis.keys(`${FASTGPT_REDIS_PREFIX}${key}:*`)).map((key) =>
key.replace(FASTGPT_REDIS_PREFIX, '')
);
return keys;
return global.redisCache;
};

View File

@@ -1,5 +1,5 @@
import type Redis from 'ioredis';
declare global {
var redisClient: Redis | null;
var redisCache: Redis | null;
}

View File

@@ -57,19 +57,14 @@ export const addLog = {
level === LogLevelEnum.error && console.error(obj);
// store log
// store
if (level >= STORE_LOG_LEVEL && connectionMongo.connection.readyState === 1) {
(async () => {
try {
await getMongoLog().create({
text: msg,
level,
metadata: obj
});
} catch (error) {
console.error('store log error', error);
}
})();
// store log
getMongoLog().create({
text: msg,
level,
metadata: obj
});
}
},
debug(msg: string, obj?: Record<string, any>) {

View File

@@ -135,14 +135,12 @@ export const llmStreamResponseToAnswerText = async (
// Tool calls
if (responseChoice?.tool_calls?.length) {
responseChoice.tool_calls.forEach((toolCall, i) => {
const index = toolCall.index ?? i;
responseChoice.tool_calls.forEach((toolCall) => {
const index = toolCall.index;
// Call new tool
const hasNewTool = toolCall?.function?.name || callingTool;
if (hasNewTool) {
// 有 function name代表新 call 工具
if (toolCall?.function?.name) {
if (toolCall.id || callingTool) {
// 有 id代表新 call 工具
if (toolCall.id) {
callingTool = {
name: toolCall.function?.name || '',
arguments: toolCall.function?.arguments || ''
@@ -223,9 +221,7 @@ export const parseReasoningContent = (text: string): [string, string] => {
};
export const removeDatasetCiteText = (text: string, retainDatasetCite: boolean) => {
return retainDatasetCite
? text.replace(/\[id\]\(CITE\)/g, '')
: text.replace(/\[([a-f0-9]{24})\](?:\([^\)]*\)?)?/g, '').replace(/\[id\]\(CITE\)/g, '');
return retainDatasetCite ? text : text.replace(/\[([a-f0-9]{24})\](?:\([^\)]*\)?)?/g, '');
};
// Parse llm stream part

View File

@@ -1,7 +1,7 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import { addLog } from '../../common/system/log';
import { retryFn } from '@fastgpt/global/common/system/utils';
@@ -41,7 +41,7 @@ export class MCPClient {
* Get available tools list
* @returns List of tools
*/
public async getTools(): Promise<McpToolConfigType[]> {
public async getTools(): Promise<ToolType[]> {
try {
const client = await this.getConnection();
const response = await client.listTools();

View File

@@ -46,7 +46,6 @@ export async function rewriteAppWorkflowToDetail({
const versionIds = appNodes
.filter((node) => node.version && Types.ObjectId.isValid(node.version))
.map((node) => node.version);
if (versionIds.length > 0) {
const versionDataList = await MongoAppVersion.find(
{

View File

@@ -61,7 +61,6 @@ const ChatItemSchema = new Schema({
type: Array,
default: []
},
errorMsg: String,
userGoodFeedback: {
type: String
},

View File

@@ -32,7 +32,6 @@ type Props = {
content: [UserChatItemType & { dataId?: string }, AIChatItemType & { dataId?: string }];
metadata?: Record<string, any>;
durationSeconds: number; //s
errorMsg?: string;
};
export async function saveChat({
@@ -51,7 +50,6 @@ export async function saveChat({
outLinkUid,
content,
durationSeconds,
errorMsg,
metadata = {}
}: Props) {
if (!chatId || chatId === 'NO_RECORD_HISTORIES') return;
@@ -106,8 +104,7 @@ export async function saveChat({
return {
...item,
[DispatchNodeResponseKeyEnum.nodeResponse]: nodeResponse,
durationSeconds,
errorMsg
durationSeconds
};
}
return item;

View File

@@ -2,9 +2,7 @@ import type {
APIFileListResponse,
ApiFileReadContentResponse,
APIFileReadResponse,
ApiDatasetDetailResponse,
APIFileServer,
APIFileItem
APIFileServer
} from '@fastgpt/global/core/dataset/apiDataset';
import axios, { type Method } from 'axios';
import { addLog } from '../../../common/system/log';
@@ -91,7 +89,7 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }
`/v1/file/list`,
{
searchKey,
parentId: parentId || apiServer.basePath
parentId
},
'POST'
);
@@ -166,34 +164,9 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }
return url;
};
const getFileDetail = async ({
apiFileId
}: {
apiFileId: string;
}): Promise<ApiDatasetDetailResponse> => {
const fileData = await request<ApiDatasetDetailResponse>(
`/v1/file/detail`,
{
id: apiFileId
},
'GET'
);
if (fileData) {
return {
id: fileData.id,
name: fileData.name,
parentId: fileData.parentId === null ? '' : fileData.parentId
};
}
return Promise.reject('File not found');
};
return {
getFileContent,
listFiles,
getFilePreviewUrl,
getFileDetail
getFilePreviewUrl
};
};

View File

@@ -474,7 +474,7 @@ export async function searchDatasetData(
).lean()
]);
const set = new Set<string>();
const set = new Map<string, number>();
const formatResult = results
.map((item, index) => {
const collection = collections.find((col) => String(col._id) === String(item.collectionId));
@@ -507,7 +507,7 @@ export async function searchDatasetData(
.filter((item) => {
if (!item) return false;
if (set.has(item.id)) return false;
set.add(item.id);
set.set(item.id, 1);
return true;
})
.map((item, index) => {
@@ -648,17 +648,7 @@ export async function searchDatasetData(
]
};
})
.filter((item) => {
if (!item) return false;
return true;
})
.map((item, index) => {
if (!item) return;
return {
...item,
score: item.score.map((item) => ({ ...item, index }))
};
}) as SearchDataResponseItemType[],
.filter(Boolean) as SearchDataResponseItemType[],
tokenLen: 0
};
};

View File

@@ -727,10 +727,9 @@ async function streamResponse({
const index = toolCall.index ?? i;
// Call new tool
const hasNewTool = toolCall?.function?.name || callingTool;
if (hasNewTool) {
// 有 function name代表新 call 工具
if (toolCall?.function?.name) {
if (toolCall.id || callingTool) {
// 有 id代表新 call 工具
if (toolCall.id) {
callingTool = {
name: toolCall.function?.name || '',
arguments: toolCall.function?.arguments || ''

View File

@@ -268,7 +268,7 @@ export async function dispatchDatasetSearch(
nodeDispatchUsages,
[DispatchNodeResponseKeyEnum.toolResponses]: {
prompt: getDatasetSearchToolResponsePrompt(),
cites: searchRes.map((item) => ({
quotes: searchRes.map((item) => ({
id: item.id,
sourceName: item.sourceName,
updateTime: item.updateTime,

View File

@@ -20,7 +20,6 @@ import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/m
import { type TeamMemberSchema } from '@fastgpt/global/support/user/team/type';
import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type';
import { getOrgIdSetWithParentByTmbId } from './org/controllers';
import { authUserSession } from '../user/session';
/** get resource permission for a team member
* If there is no permission for the team member, it will return undefined
@@ -214,6 +213,51 @@ export const delResourcePermission = ({
};
/* 下面代码等迁移 */
/* create token */
export function createJWT(user: {
_id?: string;
team?: { teamId?: string; tmbId: string };
isRoot?: boolean;
}) {
const key = process.env.TOKEN_KEY as string;
const token = jwt.sign(
{
userId: String(user._id),
teamId: String(user.team?.teamId),
tmbId: String(user.team?.tmbId),
isRoot: user.isRoot,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7
},
key
);
return token;
}
// auth token
export function authJWT(token: string) {
return new Promise<{
userId: string;
teamId: string;
tmbId: string;
isRoot: boolean;
}>((resolve, reject) => {
const key = process.env.TOKEN_KEY as string;
jwt.verify(token, key, (err, decoded: any) => {
if (err || !decoded?.userId) {
reject(ERROR_ENUM.unAuthorization);
return;
}
resolve({
userId: decoded.userId,
teamId: decoded.teamId || '',
tmbId: decoded.tmbId,
isRoot: decoded.isRoot
});
});
});
}
export async function parseHeaderCert({
req,
@@ -231,7 +275,7 @@ export async function parseHeaderCert({
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return authUserSession(cookieToken);
return await authJWT(cookieToken);
}
// from authorization get apikey
async function parseAuthorization(authorization?: string) {
@@ -301,7 +345,6 @@ export async function parseHeaderCert({
if (authToken && (token || cookie)) {
// user token(from fastgpt web)
const res = await authCookieToken(cookie, token);
return {
uid: res.userId,
teamId: res.teamId,

View File

@@ -1,179 +0,0 @@
import { retryFn } from '@fastgpt/global/common/system/utils';
import { getAllKeysByPrefix, getGlobalRedisConnection } from '../../common/redis';
import { addLog } from '../../common/system/log';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { getNanoid } from '@fastgpt/global/common/string/tools';
const redisPrefix = 'session:';
const getSessionKey = (key: string) => `${redisPrefix}${key}`;
type SessionType = {
userId: string;
teamId: string;
tmbId: string;
isRoot?: boolean;
createdAt: number;
ip?: string | null;
};
/* Session manager */
const setSession = async ({
key,
data,
expireSeconds
}: {
key: string;
data: SessionType;
expireSeconds: number;
}) => {
return await retryFn(async () => {
try {
const redis = getGlobalRedisConnection();
const formatKey = getSessionKey(key);
// 使用 hmset 存储对象字段
await redis.hmset(formatKey, {
userId: data.userId,
teamId: data.teamId,
tmbId: data.tmbId,
isRoot: data.isRoot ? '1' : '0',
createdAt: data.createdAt.toString(),
ip: data.ip
});
// 设置过期时间
if (expireSeconds) {
await redis.expire(formatKey, expireSeconds);
}
} catch (error) {
addLog.error('Set session error:', error);
return Promise.reject(error);
}
});
};
const delSession = (key: string) => {
const redis = getGlobalRedisConnection();
retryFn(() => redis.del(getSessionKey(key)));
};
const getSession = async (key: string): Promise<SessionType> => {
const formatKey = getSessionKey(key);
const redis = getGlobalRedisConnection();
// 使用 hgetall 获取所有字段
const data = await retryFn(() => redis.hgetall(formatKey));
if (!data || Object.keys(data).length === 0) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
try {
return {
userId: data.userId,
teamId: data.teamId,
tmbId: data.tmbId,
isRoot: data.isRoot === '1',
createdAt: parseInt(data.createdAt),
ip: data.ip
};
} catch (error) {
addLog.error('Parse session error:', error);
delSession(formatKey);
return Promise.reject(ERROR_ENUM.unAuthorization);
}
};
export const delUserAllSession = async (userId: string, whileList?: string[]) => {
const formatWhileList = whileList?.map((item) => getSessionKey(item));
const redis = getGlobalRedisConnection();
const keys = (await getAllKeysByPrefix(`${redisPrefix}${userId}`)).filter(
(item) => !formatWhileList?.includes(item)
);
if (keys.length > 0) {
await redis.del(keys);
}
};
// 会根据创建时间,删除超出客户端登录限制的 session
const delRedundantSession = async (userId: string) => {
// 至少为 1默认为 10
let maxSession = process.env.MAX_LOGIN_SESSION ? Number(process.env.MAX_LOGIN_SESSION) : 10;
if (maxSession < 1) {
maxSession = 1;
}
const redis = getGlobalRedisConnection();
const keys = await getAllKeysByPrefix(`${redisPrefix}${userId}`);
if (keys.length <= maxSession) {
return;
}
// 获取所有会话的创建时间
const sessionList = await Promise.all(
keys.map(async (key) => {
try {
const data = await redis.hgetall(key);
if (!data || Object.keys(data).length === 0) return null;
return {
key,
createdAt: parseInt(data.createdAt)
};
} catch (error) {
return null;
}
})
);
// 过滤掉无效数据并按创建时间排序
const validSessions = sessionList.filter(Boolean) as { key: string; createdAt: number }[];
validSessions.sort((a, b) => a.createdAt - b.createdAt);
// 删除最早创建的会话
const delKeys = validSessions.slice(0, validSessions.length - maxSession).map((item) => item.key);
if (delKeys.length > 0) {
await redis.del(delKeys);
}
};
export const createUserSession = async ({
userId,
teamId,
tmbId,
isRoot,
ip
}: {
userId: string;
teamId: string;
tmbId: string;
isRoot?: boolean;
ip?: string | null;
}) => {
const key = `${String(userId)}:${getNanoid(32)}`;
await setSession({
key,
data: {
userId: String(userId),
teamId: String(teamId),
tmbId: String(tmbId),
isRoot,
createdAt: new Date().getTime(),
ip
},
expireSeconds: 7 * 24 * 60 * 60
});
delRedundantSession(userId);
return key;
};
export const authUserSession = async (key: string): Promise<SessionType> => {
const data = await getSession(key);
return data;
};

View File

@@ -6,6 +6,7 @@ import MyTooltip from '../MyTooltip';
type Props = FlexProps & {
icon: string;
size?: string;
onClick?: () => void;
hoverColor?: string;
hoverBg?: string;
hoverBorderColor?: string;
@@ -40,9 +41,9 @@ const MyIconButton = ({
color: hoverColor,
borderColor: hoverBorderColor
}}
onClick={(e) => {
onClick={() => {
if (isLoading) return;
onClick?.(e);
onClick?.();
}}
sx={{ userSelect: 'none' }}
{...props}

View File

@@ -77,7 +77,7 @@
"owner": "owner",
"permission": "Permissions",
"permission_apikeyCreate": "Create API Key",
"permission_apikeyCreate_Tip": "You can create global APIKey and MCP services",
"permission_apikeyCreate_Tip": "Can create global APIKeys",
"permission_appCreate": "Create Application",
"permission_appCreate_tip": "Can create applications in the root directory (creation permissions in folders are controlled by the folder)",
"permission_datasetCreate": "Create Knowledge Base",

View File

@@ -18,6 +18,7 @@
"app.modules.click to update": "Click to Refresh",
"app.modules.has new version": "New Version Available",
"app.modules.not_found": "Not Found",
"app.modules.not_found_tips": "This component cannot be found in the system, please delete it, otherwise the process will not run normally",
"app.version_current": "Current Version",
"app.version_initial": "Initial Version",
"app.version_name_tips": "Version name cannot be empty",
@@ -192,7 +193,6 @@
"type.error.Workflow data is empty": "No workflow data was obtained",
"type.error.workflowresponseempty": "Response content is empty",
"type_not_recognized": "App type not recognized",
"un_auth": "No permission",
"upload_file_max_amount": "Maximum File Quantity",
"upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation",
"variable.select type_desc": "You can define a global variable that does not need to be filled in by the user.\n\nThe value of this variable can come from the API interface, the Query of the shared link, or assigned through the [Variable Update] module.",

View File

@@ -35,7 +35,6 @@
"delete_all_input_guide_confirm": "Are you sure you want to clear the input guide lexicon?",
"download_chunks": "Download data",
"empty_directory": "This directory is empty~",
"error_message": "error message",
"file_amount_over": "Exceeded maximum file quantity {{max}}",
"file_input": "File input",
"file_input_tip": "You can obtain the link to the corresponding file through the \"File Link\" of the [Plug-in Start] node",

View File

@@ -754,7 +754,6 @@
"data_index_image": "Image Index",
"data_index_question": "Inferred question index",
"data_index_summary": "Summary Index",
"data_not_found": "Data can't be found",
"dataset.Confirm move the folder": "Confirm to Move to This Directory",
"dataset.Confirm to delete the data": "Confirm to Delete This Data?",
"dataset.Confirm to delete the file": "Confirm to Delete This File and All Its Data?",
@@ -923,7 +922,6 @@
"not_open": "Not Open",
"not_permission": "The current subscription package does not support team operation logs",
"not_support": "Not Supported",
"not_support_wechat_image": "This is a WeChat picture",
"not_yet_introduced": "No Introduction Yet",
"open_folder": "Open Folder",
"option": "Option",
@@ -1135,7 +1133,7 @@
"support.wallet.subscription.AI points usage tip": "Each time the AI model is called, a certain amount of AI points will be consumed. For specific calculation standards, please refer to the 'Pricing' above.",
"support.wallet.subscription.Ai points": "AI Points Calculation Standards",
"support.wallet.subscription.Current plan": "Current Package",
"support.wallet.subscription.Extra ai points": "AI points",
"support.wallet.subscription.Extra ai points": "Extra AI Points",
"support.wallet.subscription.Extra dataset size": "Extra Dataset Capacity",
"support.wallet.subscription.Extra plan": "Extra Resource Pack",
"support.wallet.subscription.Extra plan tip": "When the standard package is not enough, you can purchase extra resource packs to continue using",
@@ -1148,7 +1146,7 @@
"support.wallet.subscription.Team plan and usage": "Package and Usage",
"support.wallet.subscription.Training weight": "Training Priority: {{weight}}",
"support.wallet.subscription.Update extra ai points": "Extra AI Points",
"support.wallet.subscription.Update extra dataset size": "Storage",
"support.wallet.subscription.Update extra dataset size": "Extra Storage",
"support.wallet.subscription.Upgrade plan": "Upgrade Package",
"support.wallet.subscription.ai_model": "AI Language Model",
"support.wallet.subscription.function.History store": "{{amount}} Days of Chat History Retention",
@@ -1157,9 +1155,9 @@
"support.wallet.subscription.function.Max dataset size": "{{amount}} Dataset Indexes",
"support.wallet.subscription.function.Max members": "{{amount}} Team Members",
"support.wallet.subscription.function.Points": "{{amount}} AI Points",
"support.wallet.subscription.mode.Month": "Month",
"support.wallet.subscription.mode.Month": "Monthly",
"support.wallet.subscription.mode.Period": "Subscription Period",
"support.wallet.subscription.mode.Year": "Year",
"support.wallet.subscription.mode.Year": "Yearly",
"support.wallet.subscription.mode.Year sale": "Two Months Free",
"support.wallet.subscription.point": "Points",
"support.wallet.subscription.standardSubLevel.custom": "Custom",
@@ -1168,7 +1166,7 @@
"support.wallet.subscription.standardSubLevel.experience": "Experience",
"support.wallet.subscription.standardSubLevel.experience_desc": "Unlock the full functionality of FastGPT",
"support.wallet.subscription.standardSubLevel.free": "Free",
"support.wallet.subscription.standardSubLevel.free desc": "Free trial of core features. \nIf you haven't logged in for 30 days, the knowledge base will be cleared.",
"support.wallet.subscription.standardSubLevel.free desc": "Basic functions can be used for free every month. If the system is not logged in for 30 consecutive days, the Dataset will be automatically cleared.",
"support.wallet.subscription.standardSubLevel.team": "Team",
"support.wallet.subscription.standardSubLevel.team_desc": "Suitable for small teams to build Dataset applications and provide external services",
"support.wallet.subscription.status.active": "Active",

View File

@@ -84,7 +84,7 @@
"owner": "所有者",
"permission": "权限",
"permission_apikeyCreate": "创建 API 密钥",
"permission_apikeyCreate_Tip": "可以创建全局的 APIKey和 MCP 服务",
"permission_apikeyCreate_Tip": "可以创建全局的 APIKey",
"permission_appCreate": "创建应用",
"permission_appCreate_tip": "可以在根目录创建应用,(文件夹下的创建权限由文件夹控制)",
"permission_datasetCreate": "创建知识库",

View File

@@ -22,6 +22,7 @@
"app.modules.click to update": "点击更新",
"app.modules.has new version": "有新版本",
"app.modules.not_found": "组件缺失",
"app.modules.not_found_tips": "系统内无法查找到该组件,请删除,否则流程无法正常运行",
"app.version_current": "当前版本",
"app.version_initial": "初始版本",
"app.version_name_tips": "版本名称不能为空",
@@ -201,7 +202,6 @@
"type.error.Workflow data is empty": "没有获取到工作流数据",
"type.error.workflowresponseempty": "响应内容为空",
"type_not_recognized": "未识别到应用类型",
"un_auth": "无权限",
"upload_file_max_amount": "最大文件数量",
"upload_file_max_amount_tip": "单轮对话中最大上传文件数量",
"variable.select type_desc": "可以为工作流定义全局变量,常用临时缓存。赋值的方式包括:\n1. 从对话页面的 query 参数获取。\n2. 通过 API 的 variables 对象传递。\n3. 通过【变量更新】节点进行赋值。",

View File

@@ -35,7 +35,6 @@
"delete_all_input_guide_confirm": "确定要清空输入引导词库吗?",
"download_chunks": "下载数据",
"empty_directory": "这个目录已经没东西可选了~",
"error_message": "错误信息",
"file_amount_over": "超出最大文件数量 {{max}}",
"file_input": "系统文件",
"file_input_tip": "可通过【插件开始】节点的“文件链接”获取对应文件的链接",

View File

@@ -753,7 +753,6 @@
"data_index_image": "图片索引",
"data_index_question": "推测问题索引",
"data_index_summary": "摘要索引",
"data_not_found": "数据找不到了",
"dataset.Confirm move the folder": "确认移动到该目录",
"dataset.Confirm to delete the data": "确认删除该数据?",
"dataset.Confirm to delete the file": "确认删除该文件及其所有数据?",
@@ -922,7 +921,6 @@
"not_open": "未开启",
"not_permission": "当前订阅套餐不支持团队操作日志",
"not_support": "不支持",
"not_support_wechat_image": "这是一张微信图片",
"not_yet_introduced": "暂无介绍",
"open_folder": "打开文件夹",
"option": "选项",
@@ -1167,7 +1165,7 @@
"support.wallet.subscription.standardSubLevel.experience": "体验版",
"support.wallet.subscription.standardSubLevel.experience_desc": "可解锁 FastGPT 完整功能",
"support.wallet.subscription.standardSubLevel.free": "免费版",
"support.wallet.subscription.standardSubLevel.free desc": "核心功能免费试用。30 天未登录,将会清空知识库",
"support.wallet.subscription.standardSubLevel.free desc": "每月均可免费使用基础功能,连续 30 天未登录系统,将会自动清除知识库",
"support.wallet.subscription.standardSubLevel.team": "团队版",
"support.wallet.subscription.standardSubLevel.team_desc": "适合小团队构建知识库应用并提供对外服务",
"support.wallet.subscription.status.active": "生效中",

View File

@@ -77,7 +77,7 @@
"owner": "擁有者",
"permission": "權限",
"permission_apikeyCreate": "建立 API 密鑰",
"permission_apikeyCreate_Tip": "可以創建全局的 APIKey和 MCP 服務",
"permission_apikeyCreate_Tip": "可以建立全域的 APIKey",
"permission_appCreate": "建立應用程式",
"permission_appCreate_tip": "可以在根目錄建立應用程式,(資料夾下的建立權限由資料夾控制)",
"permission_datasetCreate": "建立知識庫",

View File

@@ -18,6 +18,7 @@
"app.modules.click to update": "點選更新",
"app.modules.has new version": "有新版本",
"app.modules.not_found": "元件遺失",
"app.modules.not_found_tips": "系統內無法查詢到該元件,請刪除,否則流程無法正常運作",
"app.version_current": "目前版本",
"app.version_initial": "初始版本",
"app.version_name_tips": "版本名稱不能空白",
@@ -192,7 +193,6 @@
"type.error.Workflow data is empty": "沒有獲取到工作流數據",
"type.error.workflowresponseempty": "響應內容為空",
"type_not_recognized": "未識別到應用程式類型",
"un_auth": "無權限",
"upload_file_max_amount": "最大檔案數量",
"upload_file_max_amount_tip": "單輪對話中最大上傳檔案數量",
"variable.select type_desc": "可以為工作流程定義全域變數,常用於暫存。賦值的方式包括:\n1. 從對話頁面的 query 參數取得。\n2. 透過 API 的 variables 物件傳遞。\n3. 透過【變數更新】節點進行賦值。",

View File

@@ -33,7 +33,6 @@
"delete_all_input_guide_confirm": "確定要清除輸入導引詞彙庫嗎?",
"download_chunks": "下載資料",
"empty_directory": "此目錄中已無項目可選~",
"error_message": "錯誤訊息",
"file_amount_over": "超出檔案數量上限 {{max}}",
"file_input": "檔案輸入",
"file_input_tip": "可透過「外掛程式啟動」節點的「檔案連結」取得對應檔案的連結",

View File

@@ -753,7 +753,6 @@
"data_index_image": "圖片索引",
"data_index_question": "推測問題索引",
"data_index_summary": "摘要索引",
"data_not_found": "數據找不到了",
"dataset.Confirm move the folder": "確認移動到此目錄",
"dataset.Confirm to delete the data": "確認刪除此資料?",
"dataset.Confirm to delete the file": "確認刪除此檔案及其所有資料?",
@@ -922,7 +921,6 @@
"not_open": "未開啟",
"not_permission": "當前訂閱套餐不支持團隊操作日誌",
"not_support": "不支援",
"not_support_wechat_image": "這是一張微信圖片",
"not_yet_introduced": "暫無介紹",
"open_folder": "開啟資料夾",
"option": "選項",
@@ -1167,7 +1165,7 @@
"support.wallet.subscription.standardSubLevel.experience": "體驗版",
"support.wallet.subscription.standardSubLevel.experience_desc": "可解鎖 FastGPT 完整功能",
"support.wallet.subscription.standardSubLevel.free": "免費版",
"support.wallet.subscription.standardSubLevel.free desc": "核心功能免費試用。 \n30 天未登錄,將會清空知識庫",
"support.wallet.subscription.standardSubLevel.free desc": "每月可免費使用基本功能。若連續 30 天未登入系統,系統將自動清除知識庫",
"support.wallet.subscription.standardSubLevel.team": "團隊版",
"support.wallet.subscription.standardSubLevel.team_desc": "適合小團隊建構知識庫應用並提供對外服務",
"support.wallet.subscription.status.active": "使用中",

View File

@@ -5318,10 +5318,9 @@
}
},
"node_modules/undici": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"license": "MIT",
"version": "6.21.1",
"resolved": "https://registry.npmmirror.com/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"engines": {
"node": ">=18.17"
}

View File

@@ -3,6 +3,8 @@ LOG_DEPTH=3
DEFAULT_ROOT_PSW=123456
# 数据库最大连接数
DB_MAX_LINK=5
# token
TOKEN_KEY=dfdasfdas
# 文件阅读时的密钥
FILE_TOKEN_KEY=filetokenkey
# root key, 最高权限
@@ -63,8 +65,6 @@ CHECK_INTERNAL_IP=false
PASSWORD_LOGIN_LOCK_SECONDS=
# 密码过期月份,不设置则不会过期
PASSWORD_EXPIRED_MONTH=
# 最大登录客户端数量,默认为 10
MAX_LOGIN_SESSION=
# 特殊配置
# 自定义跨域,不配置时,默认都允许跨域(逗号分割)

View File

@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.9.9",
"version": "4.9.8",
"private": false,
"scripts": {
"dev": "next dev",

View File

@@ -22,184 +22,135 @@ import { getCollectionSourceData } from '@fastgpt/global/core/dataset/collection
import Markdown from '.';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { Types } from 'mongoose';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { useCreation } from 'ahooks';
export type AProps = {
chatAuthData?: {
appId: string;
chatId: string;
chatItemDataId: string;
} & OutLinkChatAuthProps;
onOpenCiteModal?: (e?: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
quoteId?: string;
}) => void;
};
const EmptyHrefLink = function EmptyHrefLink({ content }: { content: string }) {
const { t } = useTranslation();
return (
<MyTooltip label={t('common:core.chat.markdown.Quick Question')}>
<Button
variant={'whitePrimary'}
size={'xs'}
borderRadius={'md'}
my={1}
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text: content })}
>
{content}
</Button>
</MyTooltip>
);
};
const CiteLink = React.memo(function CiteLink({
id,
chatAuthData,
onOpenCiteModal,
showAnimation
}: { id: string; showAnimation?: boolean } & AProps) {
const A = ({ children, chatAuthData, showAnimation, ...props }: any) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
if (!Types.ObjectId.isValid(id)) {
return <></>;
}
const {
data: datasetCiteData,
loading,
runAsync: getQuoteDataById
} = useRequest2((id: string) => getQuoteData({ id, ...chatAuthData }), {
manual: true
});
const sourceData = useMemo(
() => getCollectionSourceData(datasetCiteData?.collection),
[datasetCiteData?.collection]
);
const icon = useMemo(
() => getSourceNameIcon({ sourceId: sourceData.sourceId, sourceName: sourceData.sourceName }),
[sourceData]
);
return (
<Popover
isLazy
direction="rtl"
placement="bottom"
strategy={'fixed'}
isOpen={isOpen}
onClose={onClose}
onOpen={() => {
onOpen();
if (showAnimation) return;
getQuoteDataById(id);
}}
trigger={'hover'}
gutter={4}
>
<PopoverTrigger>
<Button variant={'unstyled'} minH={0} minW={0} h={'auto'}>
<MyIcon
name={'core/chat/quoteSign'}
w={'1rem'}
color={'primary.700'}
cursor={'pointer'}
/>
</Button>
</PopoverTrigger>
<PopoverContent boxShadow={'lg'} w={'500px'} maxW={'90vw'} py={4}>
<MyBox isLoading={loading || showAnimation}>
<PopoverArrow />
<PopoverBody py={0} px={0} fontSize={'sm'}>
<Flex px={4} pb={1} justifyContent={'space-between'}>
<Box
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
borderRadius={'sm'}
overflow={'hidden'}
display={'inline-flex'}
height={6}
mr={1}
>
<Flex px={1.5}>
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box
className={'textEllipsis'}
wordBreak={'break-all'}
flex={'1 0 0'}
fontSize={'mini'}
color={'myGray.900'}
>
{sourceData.sourceName}
</Box>
</Flex>
</Box>
<Button
variant={'ghost'}
color={'primary.600'}
size={'xs'}
onClick={() => {
onClose();
onOpenCiteModal?.({
quoteId: id,
sourceId: sourceData.sourceId,
sourceName: sourceData.sourceName,
datasetId: datasetCiteData?.collection.datasetId,
collectionId: datasetCiteData?.collection._id
});
}}
>
{t('common:all_quotes')}
</Button>
</Flex>
<Box h={'300px'} overflow={'auto'} px={4}>
<Markdown source={datasetCiteData?.q} />
{datasetCiteData?.a && <Markdown source={datasetCiteData?.a} />}
</Box>
</PopoverBody>
</MyBox>
</PopoverContent>
</Popover>
);
});
const A = ({
children,
chatAuthData,
onOpenCiteModal,
showAnimation,
...props
}: AProps & {
children: any;
showAnimation: boolean;
[key: string]: any;
}) => {
const content = useCreation(() => String(children), [children]);
const content = useMemo(() => String(children), [children]);
// empty href link
if (!props.href && typeof children?.[0] === 'string') {
return <EmptyHrefLink content={content} />;
return (
<MyTooltip label={t('common:core.chat.markdown.Quick Question')}>
<Button
variant={'whitePrimary'}
size={'xs'}
borderRadius={'md'}
my={1}
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text: content })}
>
{content}
</Button>
</MyTooltip>
);
}
// Cite
if (
(props.href?.startsWith('CITE') || props.href?.startsWith('QUOTE')) &&
typeof content === 'string'
typeof children?.[0] === 'string'
) {
if (!Types.ObjectId.isValid(content)) {
return <></>;
}
const {
data: quoteData,
loading,
runAsync: getQuoteDataById
} = useRequest2((id: string) => getQuoteData({ id, ...chatAuthData }), {
manual: true
});
const sourceData = useMemo(
() => getCollectionSourceData(quoteData?.collection),
[quoteData?.collection]
);
const icon = useMemo(
() => getSourceNameIcon({ sourceId: sourceData.sourceId, sourceName: sourceData.sourceName }),
[sourceData]
);
return (
<CiteLink
id={content}
chatAuthData={chatAuthData}
onOpenCiteModal={onOpenCiteModal}
showAnimation={showAnimation}
/>
<Popover
isLazy
direction="rtl"
placement="bottom"
strategy={'fixed'}
isOpen={isOpen}
onClose={onClose}
onOpen={() => {
onOpen();
if (showAnimation) return;
getQuoteDataById(String(children));
}}
trigger={'hover'}
gutter={4}
>
<PopoverTrigger>
<Button variant={'unstyled'} minH={0} minW={0} h={'auto'}>
<MyIcon
name={'core/chat/quoteSign'}
w={'1rem'}
color={'primary.700'}
cursor={'pointer'}
/>
</Button>
</PopoverTrigger>
<PopoverContent boxShadow={'lg'} w={'500px'} maxW={'90vw'} py={4}>
<MyBox isLoading={loading || showAnimation}>
<PopoverArrow />
<PopoverBody py={0} px={0} fontSize={'sm'}>
<Flex px={4} pb={1} justifyContent={'space-between'}>
<Box
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
borderRadius={'sm'}
overflow={'hidden'}
display={'inline-flex'}
height={6}
mr={1}
>
<Flex px={1.5}>
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box
className={'textEllipsis'}
wordBreak={'break-all'}
flex={'1 0 0'}
fontSize={'mini'}
color={'myGray.900'}
>
{sourceData.sourceName}
</Box>
</Flex>
</Box>
<Button
variant={'ghost'}
color={'primary.600'}
size={'xs'}
onClick={() => {
onClose();
eventBus.emit(EventNameEnum.openQuoteReader, {
quoteId: String(children),
sourceId: sourceData.sourceId,
sourceName: sourceData.sourceName,
datasetId: quoteData?.collection.datasetId,
collectionId: quoteData?.collection._id
});
}}
>
{t('common:all_quotes')}
</Button>
</Flex>
<Box h={'300px'} overflow={'auto'} px={4}>
<Markdown source={quoteData?.q} />
{quoteData?.a && <Markdown source={quoteData?.a} />}
</Box>
</PopoverBody>
</MyBox>
</PopoverContent>
</Popover>
);
}

View File

@@ -2,10 +2,8 @@ import React, { useState } from 'react';
import { Box, type ImageProps, Skeleton } from '@chakra-ui/react';
import MyPhotoView from '@fastgpt/web/components/common/Image/PhotoView';
import { useBoolean } from 'ahooks';
import { useTranslation } from 'next-i18next';
const MdImage = ({ src, ...props }: { src?: string } & ImageProps) => {
const { t } = useTranslation();
const [isLoaded, { setTrue }] = useBoolean(false);
const [renderSrc, setRenderSrc] = useState(src);
@@ -13,11 +11,6 @@ const MdImage = ({ src, ...props }: { src?: string } & ImageProps) => {
if (src?.includes('base64') && !src.startsWith('data:image')) {
return <Box>Invalid base64 image</Box>;
}
if (props.alt?.startsWith('OFFIACCOUNT_MEDIA')) {
return <Box>{t('common:not_support_wechat_image')}</Box>;
}
return (
<Skeleton isLoaded={isLoaded}>
<MyPhotoView

View File

@@ -13,7 +13,7 @@ import dynamic from 'next/dynamic';
import { Box } from '@chakra-ui/react';
import { CodeClassNameEnum, mdTextFormat } from './utils';
import { useCreation } from 'ahooks';
import type { AProps } from './A';
import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
const CodeLight = dynamic(() => import('./codeBlock/CodeLight'), { ssr: false });
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
@@ -33,7 +33,12 @@ type Props = {
showAnimation?: boolean;
isDisabled?: boolean;
forbidZhFormat?: boolean;
} & AProps;
chatAuthData?: {
appId: string;
chatId: string;
chatItemDataId: string;
} & OutLinkChatAuthProps;
};
const Markdown = (props: Props) => {
const source = props.source || '';
@@ -48,25 +53,16 @@ const MarkdownRender = ({
showAnimation,
isDisabled,
forbidZhFormat,
chatAuthData,
onOpenCiteModal
chatAuthData
}: Props) => {
const components = useCreation(() => {
return {
img: Image,
pre: RewritePre,
code: Code,
a: (props: any) => (
<A
{...props}
showAnimation={showAnimation}
chatAuthData={chatAuthData}
onOpenCiteModal={onOpenCiteModal}
/>
)
a: (props: any) => <A {...props} showAnimation={showAnimation} chatAuthData={chatAuthData} />
};
}, [chatAuthData, onOpenCiteModal, showAnimation]);
}, [chatAuthData, showAnimation]);
const formatSource = useMemo(() => {
if (showAnimation || forbidZhFormat) return source;

View File

@@ -28,16 +28,10 @@ import { isEqual } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
import {
ChatItemContext,
type OnOpenCiteModalProps
} from '@/web/core/chat/context/chatItemContext';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import dynamic from 'next/dynamic';
import { useMemoizedFn } from 'ahooks';
import ChatBoxDivider from '../../../Divider';
const ResponseTags = dynamic(() => import('./ResponseTags'));
import { useToast } from '@fastgpt/web/hooks/useToast';
const colorMap = {
[ChatStatusEnum.loading]: {
@@ -94,15 +88,13 @@ const AIContentCard = React.memo(function AIContentCard({
dataId,
isLastChild,
isChatting,
questionGuides,
onOpenCiteModal
questionGuides
}: {
dataId: string;
chatValue: ChatItemValueItemType[];
isLastChild: boolean;
isChatting: boolean;
questionGuides: string[];
onOpenCiteModal: (e?: OnOpenCiteModalProps) => void;
}) {
return (
<Flex flexDirection={'column'} gap={2}>
@@ -116,7 +108,6 @@ const AIContentCard = React.memo(function AIContentCard({
value={value}
isLastResponseValue={isLastChild && i === chatValue.length - 1}
isChatting={isChatting}
onOpenCiteModal={onOpenCiteModal}
/>
);
})}
@@ -131,6 +122,7 @@ const ChatItem = (props: Props) => {
const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props;
const { isPc } = useSystem();
const { toast } = useToast();
const styleMap: BoxProps = {
...(type === ChatRoleEnum.Human
@@ -158,6 +150,7 @@ const ChatItem = (props: Props) => {
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showNodeStatus = useContextSelector(ChatItemContext, (v) => v.showNodeStatus);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
@@ -235,48 +228,64 @@ const ChatItem = (props: Props) => {
return groupedValues;
}, [chat.obj, chat.value, isChatting]);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const onOpenCiteModal = useMemoizedFn(
(item?: {
const handleOpenQuoteReader = useCallback(
({
collectionId,
sourceId,
sourceName,
datasetId,
quoteId
}: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
quoteId?: string;
}) => {
const collectionIdList = item?.collectionId
? [item.collectionId]
if (!setQuoteData) return;
const collectionIdList = collectionId
? [collectionId]
: [...new Set(quoteList.map((item) => item.collectionId))];
setCiteModalData({
setQuoteData({
rawSearch: quoteList,
metadata:
item?.collectionId && isShowReadRawSource
collectionId && isShowReadRawSource
? {
appId: appId,
chatId: chatId,
chatItemDataId: chat.dataId,
collectionId: item.collectionId,
collectionId: collectionId,
collectionIdList,
sourceId: item.sourceId || '',
sourceName: item.sourceName || '',
datasetId: item.datasetId || '',
sourceId: sourceId || '',
sourceName: sourceName || '',
datasetId: datasetId || '',
outLinkAuthData,
quoteId: item.quoteId
quoteId
}
: {
appId: appId,
chatId: chatId,
chatItemDataId: chat.dataId,
collectionIdList,
sourceId: item?.sourceId,
sourceName: item?.sourceName,
sourceId: sourceId,
sourceName: sourceName,
outLinkAuthData
}
});
}
},
[setQuoteData, quoteList, isShowReadRawSource, appId, chatId, chat.dataId, outLinkAuthData]
);
useEffect(() => {
if (chat.obj !== ChatRoleEnum.AI) return;
eventBus.on(EventNameEnum.openQuoteReader, handleOpenQuoteReader);
return () => {
eventBus.off(EventNameEnum.openQuoteReader);
};
}, [chat.obj, handleOpenQuoteReader]);
return (
<Box
_hover={{
@@ -353,39 +362,16 @@ const ChatItem = (props: Props) => {
>
{type === ChatRoleEnum.Human && <HumanContentCard chatValue={value} />}
{type === ChatRoleEnum.AI && (
<>
<AIContentCard
chatValue={value}
dataId={chat.dataId}
isLastChild={isLastChild && i === splitAiResponseResults.length - 1}
isChatting={isChatting}
questionGuides={questionGuides}
onOpenCiteModal={onOpenCiteModal}
/>
{i === splitAiResponseResults.length - 1 && (
<ResponseTags
showTags={!isLastChild || !isChatting}
historyItem={chat}
onOpenCiteModal={onOpenCiteModal}
/>
)}
</>
<AIContentCard
chatValue={value}
dataId={chat.dataId}
isLastChild={isLastChild && i === splitAiResponseResults.length - 1}
isChatting={isChatting}
questionGuides={questionGuides}
/>
)}
{/* Example: Response tags. A set of dialogs only needs to be displayed once*/}
{i === splitAiResponseResults.length - 1 && (
<>
{/* error message */}
{!!chat.errorMsg && (
<Box mt={2}>
<ChatBoxDivider icon={'common/errorFill'} text={t('chat:error_message')} />
<Box fontSize={'xs'} color={'myGray.500'}>
{chat.errorMsg}
</Box>
</Box>
)}
{children}
</>
)}
{i === splitAiResponseResults.length - 1 && <>{children}</>}
{/* 对话框底部的复制按钮 */}
{type == ChatRoleEnum.AI &&
value[0]?.type !== 'interactive' &&

View File

@@ -14,24 +14,17 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useSize } from 'ahooks';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
const ContextModal = dynamic(() => import('./ContextModal'));
const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal'));
const ResponseTags = ({
showTags,
historyItem,
onOpenCiteModal
historyItem
}: {
showTags: boolean;
historyItem: ChatSiteItemType;
onOpenCiteModal: (e?: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
quoteId?: string;
}) => void;
}) => {
const { isPc } = useSystem();
const { t } = useTranslation();
@@ -87,6 +80,15 @@ const ResponseTags = ({
}));
}, [quoteList]);
const openQuoteReader = (item?: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
}) => {
eventBus.emit(EventNameEnum.openQuoteReader, item);
};
const notEmptyTags =
quoteList.length > 0 ||
(llmModuleAccount === 1 && notSharePage) ||
@@ -159,7 +161,7 @@ const ResponseTags = ({
cursor={'pointer'}
onClick={(e) => {
e.stopPropagation();
onOpenCiteModal(item);
openQuoteReader(item);
}}
height={6}
>
@@ -214,7 +216,7 @@ const ResponseTags = ({
cursor={'pointer'}
onClick={(e) => {
e.stopPropagation();
onOpenCiteModal();
openQuoteReader();
}}
>
{t('chat:citations', { num: quoteList.length })}

View File

@@ -68,6 +68,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal'));
const SelectMarkCollection = dynamic(() => import('./components/SelectMarkCollection'));
@@ -553,7 +554,7 @@ const ChatBox = ({
const responseData = mergeChatResponseData(item.responseData || []);
if (responseData[responseData.length - 1]?.error) {
toast({
title: t(getErrText(responseData[responseData.length - 1].error)),
title: t(responseData[responseData.length - 1].error?.message),
status: 'error'
});
}
@@ -1013,6 +1014,10 @@ const ChatBox = ({
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatRecords.length - 1 || !isChatting}
historyItem={item}
/>
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
@@ -1067,6 +1072,7 @@ const ChatBox = ({
chatType,
delOneMessage,
externalVariableList?.length,
isChatting,
onAddUserDislike,
onAddUserLike,
onCloseCustomFeedback,

View File

@@ -22,7 +22,6 @@ import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants';
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
type PluginRunContextType = PluginRunBoxProps & {
isChatting: boolean;
@@ -259,7 +258,7 @@ const PluginRunContextProvider = ({
const responseData = mergeChatResponseData(item.responseData || []);
if (responseData[responseData.length - 1]?.error) {
toast({
title: t(getErrText(responseData[responseData.length - 1].error)),
title: t(responseData[responseData.length - 1].error?.message),
status: 'error'
});
}

View File

@@ -30,7 +30,7 @@ import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { useContextSelector } from 'use-context-selector';
import { type OnOpenCiteModalProps } from '@/web/core/chat/context/chatItemContext';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
import { useCreation } from 'ahooks';
@@ -90,13 +90,11 @@ const RenderResoningContent = React.memo(function RenderResoningContent({
const RenderText = React.memo(function RenderText({
showAnimation,
text,
chatItemDataId,
onOpenCiteModal
chatItemDataId
}: {
showAnimation: boolean;
text: string;
chatItemDataId: string;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
}) {
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
@@ -113,14 +111,7 @@ const RenderText = React.memo(function RenderText({
return { appId, chatId, chatItemDataId, ...outLinkAuthData };
}, [appId, chatId, chatItemDataId, outLinkAuthData]);
return (
<Markdown
source={source}
showAnimation={showAnimation}
chatAuthData={chatAuthData}
onOpenCiteModal={onOpenCiteModal}
/>
);
return <Markdown source={source} showAnimation={showAnimation} chatAuthData={chatAuthData} />;
});
const RenderTool = React.memo(
@@ -249,14 +240,12 @@ const AIResponseBox = ({
chatItemDataId,
value,
isLastResponseValue,
isChatting,
onOpenCiteModal
isChatting
}: {
chatItemDataId: string;
value: UserChatItemValueItemType | AIChatItemValueItemType;
isLastResponseValue: boolean;
isChatting: boolean;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
}) => {
if (value.type === ChatItemValueTypeEnum.text && value.text) {
return (
@@ -264,7 +253,6 @@ const AIResponseBox = ({
chatItemDataId={chatItemDataId}
showAnimation={isChatting && isLastResponseValue}
text={value.text.content}
onOpenCiteModal={onOpenCiteModal}
/>
);
}

View File

@@ -38,8 +38,8 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@@ -81,7 +81,7 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
right={0}
h={['100%', '96%']}
w={'100%'}
maxW={datasetCiteData ? ['100%', '1080px'] : ['100%', '600px']}
maxW={quoteData ? ['100%', '1080px'] : ['100%', '600px']}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
@@ -169,7 +169,7 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
)}
</Box>
{datasetCiteData && (
{quoteData && (
<Box
flex={'1 0 0'}
w={0}
@@ -183,9 +183,9 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
borderRadius={'md'}
>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</Box>
)}

View File

@@ -7,7 +7,7 @@ import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext
import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
import { cardStyles } from '../constants';
import { useTranslation } from 'react-i18next';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { Controller, useForm } from 'react-hook-form';
@@ -19,7 +19,7 @@ import { postRunMCPTool } from '@/web/core/app/api/plugin';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const ChatTest = ({ currentTool, url }: { currentTool: McpToolConfigType | null; url: string }) => {
const ChatTest = ({ currentTool, url }: { currentTool: ToolType | null; url: string }) => {
const { t } = useTranslation();
const [output, setOutput] = useState<string>('');
@@ -135,7 +135,7 @@ const ChatTest = ({ currentTool, url }: { currentTool: McpToolConfigType | null;
);
};
const Render = ({ currentTool, url }: { currentTool: McpToolConfigType | null; url: string }) => {
const Render = ({ currentTool, url }: { currentTool: ToolType | null; url: string }) => {
const { chatId } = useChatStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
@@ -178,7 +178,7 @@ const RenderToolInput = ({
type: string;
description?: string;
};
toolData: McpToolConfigType | null;
toolData: ToolType | null;
value: any;
onChange: (value: any) => void;
isInvalid: boolean;

View File

@@ -7,7 +7,7 @@ import AppCard from './AppCard';
import ChatTest from './ChatTest';
import MyBox from '@fastgpt/web/components/common/MyBox';
import EditForm from './EditForm';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
const Edit = ({
url,
@@ -19,10 +19,10 @@ const Edit = ({
}: {
url: string;
setUrl: (url: string) => void;
toolList: McpToolConfigType[];
setToolList: (toolList: McpToolConfigType[]) => void;
currentTool: McpToolConfigType | null;
setCurrentTool: (tool: McpToolConfigType) => void;
toolList: ToolType[];
setToolList: (toolList: ToolType[]) => void;
currentTool: ToolType | null;
setCurrentTool: (tool: ToolType) => void;
}) => {
const { isPc } = useSystem();

View File

@@ -7,7 +7,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { AppContext } from '../context';
import { useContextSelector } from 'use-context-selector';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
@@ -24,14 +24,14 @@ const EditForm = ({
}: {
url: string;
setUrl: (url: string) => void;
toolList: McpToolConfigType[];
setToolList: (toolList: McpToolConfigType[]) => void;
currentTool: McpToolConfigType | null;
setCurrentTool: (tool: McpToolConfigType) => void;
toolList: ToolType[];
setToolList: (toolList: ToolType[]) => void;
currentTool: ToolType | null;
setCurrentTool: (tool: ToolType) => void;
}) => {
const { t } = useTranslation();
const [toolDetail, setToolDetail] = useState<McpToolConfigType | null>(null);
const [toolDetail, setToolDetail] = useState<ToolType | null>(null);
const { runAsync: runGetMCPTools, loading: isGettingTools } = useRequest2(
async (data: getMCPToolsBody) => await getMCPTools(data),
@@ -109,10 +109,6 @@ const EditForm = ({
boxShadow:
'0px 4px 4px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}}
cursor={'pointer'}
onClick={() => {
setCurrentTool(tool);
}}
>
<Flex alignItems={'center'} py={2} px={3}>
<Box w={'20px'} fontSize={'14px'} color={'myGray.500'} fontWeight={'medium'}>
@@ -161,11 +157,23 @@ const EditForm = ({
hoverBg={'rgba(51, 112, 255, 0.10)'}
hoverBorderColor={'primary.300'}
tip={t('app:MCP_tools_detail')}
onClick={(e) => {
e.stopPropagation();
onClick={() => {
setToolDetail(tool);
}}
/>
<MyIconButton
size={'16px'}
icon={'core/workflow/debug'}
p={2}
border={'1px solid'}
borderColor={'myGray.250'}
hoverBg={'rgba(51, 112, 255, 0.10)'}
hoverBorderColor={'primary.300'}
tip={t('app:MCP_tools_debug')}
onClick={() => {
setCurrentTool(tool);
}}
/>
</Flex>
</MyBox>
);
@@ -180,7 +188,7 @@ const EditForm = ({
export default React.memo(EditForm);
const ToolDetailModal = ({ tool, onClose }: { tool: McpToolConfigType; onClose: () => void }) => {
const ToolDetailModal = ({ tool, onClose }: { tool: ToolType; onClose: () => void }) => {
const { t } = useTranslation();
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);

View File

@@ -8,10 +8,10 @@ import { getAppFolderPath } from '@/web/core/app/api/app';
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import { postUpdateMCPTools } from '@/web/core/app/api/plugin';
const Header = ({ url, toolList }: { url: string; toolList: McpToolConfigType[] }) => {
const Header = ({ url, toolList }: { url: string; toolList: ToolType[] }) => {
const { t } = useTranslation();
const appId = useContextSelector(AppContext, (v) => v.appId);
const router = useRouter();

View File

@@ -5,7 +5,7 @@ import Edit from './Edit';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import { type MCPToolSetData } from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
const MCPTools = () => {
@@ -18,10 +18,8 @@ const MCPTools = () => {
}, [appDetail.modules]);
const [url, setUrl] = useState(toolSetData?.url || '');
const [toolList, setToolList] = useState<McpToolConfigType[]>(toolSetData?.toolList || []);
const [currentTool, setCurrentTool] = useState<McpToolConfigType | null>(
toolSetData?.toolList[0] || null
);
const [toolList, setToolList] = useState<ToolType[]>(toolSetData?.toolList || []);
const [currentTool, setCurrentTool] = useState<ToolType | null>(toolSetData?.toolList[0] || null);
return (
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>

View File

@@ -26,8 +26,8 @@ const ChatTest = ({ appForm, setRenderEdit }: Props) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
// form2AppWorkflow dependent allDatasets
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
@@ -42,8 +42,8 @@ const ChatTest = ({ appForm, setRenderEdit }: Props) => {
}, [appForm, setWorkflowData, t]);
useEffect(() => {
setRenderEdit(!datasetCiteData);
}, [datasetCiteData, setRenderEdit]);
setRenderEdit(!quoteData);
}, [quoteData, setRenderEdit]);
const { ChatContainer, restartChat, loading } = useChatTest({
...workflowData,
@@ -89,12 +89,12 @@ const ChatTest = ({ appForm, setRenderEdit }: Props) => {
<ChatContainer />
</Box>
</MyBox>
{datasetCiteData && (
{quoteData && (
<Box flex={'1 0 0'} w={0} maxW={'560px'} {...cardStyles} boxShadow={'3'}>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</Box>
)}

View File

@@ -17,7 +17,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import ConfigToolModal from './ConfigToolModal';
import { getWebLLMModel } from '@/web/common/system/utils';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { formatToolError } from '@fastgpt/global/core/app/utils';
import { checkAppUnExistError } from '@fastgpt/global/core/app/utils';
const ToolSelect = ({
appForm,
@@ -65,7 +65,7 @@ const ToolSelect = ({
gridGap={[2, 4]}
>
{appForm.selectedTools.map((item) => {
const toolError = formatToolError(item.pluginData?.error);
const hasError = checkAppUnExistError(item.pluginData?.error);
return (
<MyTooltip key={item.id} label={item.intro}>
@@ -77,10 +77,10 @@ const ToolSelect = ({
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
borderColor={toolError ? 'red.600' : ''}
borderColor={hasError ? 'red.600' : ''}
_hover={{
...hoverDeleteStyles,
borderColor: toolError ? 'red.600' : 'primary.300'
borderColor: hasError ? 'red.600' : 'primary.300'
}}
cursor={'pointer'}
onClick={() => {
@@ -93,7 +93,7 @@ const ToolSelect = ({
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
) ||
toolError ||
hasError ||
item.flowNodeType === FlowNodeTypeEnum.tool ||
item.flowNodeType === FlowNodeTypeEnum.toolSet
) {
@@ -113,19 +113,21 @@ const ToolSelect = ({
>
{item.name}
</Box>
{toolError && (
<Flex
bg={'red.50'}
alignItems={'center'}
h={6}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
>
<MyIcon name={'common/errorFill'} w={'14px'} mr={1} />
<Box color={'red.600'}>{t(toolError as any)}</Box>
</Flex>
{hasError && (
<MyTooltip label={t('app:app.modules.not_found_tips')}>
<Flex
bg={'red.50'}
alignItems={'center'}
h={6}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
>
<MyIcon name={'common/errorFill'} w={'14px'} mr={1} />
<Box color={'red.600'}>{t('app:app.modules.not_found')}</Box>
</Flex>
</MyTooltip>
)}
<DeleteIcon
ml={2}

View File

@@ -43,8 +43,8 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
});
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
@@ -60,7 +60,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
bottom={0}
right={0}
onClick={() => {
setCiteModalData(undefined);
setQuoteData(undefined);
onClose();
}}
/>
@@ -72,7 +72,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
top={5}
right={0}
h={isOpen ? '95%' : '0'}
w={isOpen ? (datasetCiteData ? ['100%', '960px'] : ['100%', '460px']) : '0'}
w={isOpen ? (quoteData ? ['100%', '960px'] : ['100%', '460px']) : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
@@ -152,7 +152,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
<ChatContainer />
</Box>
{datasetCiteData && (
{quoteData && (
<Box
flex={'1 0 0'}
w={0}
@@ -166,9 +166,9 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
borderRadius={'md'}
>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</Box>
)}

View File

@@ -5,15 +5,14 @@ import NodeCard from './render/NodeCard';
import IOTitle from '../components/IOTitle';
import Container from '../components/Container';
import { useTranslation } from 'react-i18next';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import { Box, Flex } from '@chakra-ui/react';
const NodeToolSet = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { inputs } = data;
const toolList: McpToolConfigType[] = inputs.find((item) => item.key === 'toolSetData')?.value
?.toolList;
const toolList: ToolType[] = inputs.find((item) => item.key === 'toolSetData')?.value?.toolList;
return (
<NodeCard minW={'350px'} selected={selected} {...data}>

View File

@@ -35,7 +35,6 @@ import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useCreation } from 'ahooks';
import { formatToolError } from '@fastgpt/global/core/app/utils';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
@@ -145,7 +144,6 @@ const NodeCard = (props: Props) => {
/* Node header */
const Header = useMemo(() => {
const showHeader = node?.flowNodeType !== FlowNodeTypeEnum.comment;
const error = formatToolError(node?.pluginData?.error);
return (
<Box position={'relative'}>
@@ -256,19 +254,23 @@ const NodeCard = (props: Props) => {
)}
</UseGuideModal>
)}
{!!error && (
<Flex
bg={'red.50'}
alignItems={'center'}
h={8}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
>
<MyIcon name={'common/errorFill'} w={'14px'} mr={1} />
<Box color={'red.600'}>{t(error as any)}</Box>
</Flex>
{!!node?.pluginData?.error && (
<MyTooltip label={node?.pluginData?.error || t('app:app.modules.not_found_tips')}>
<Flex
bg={'red.50'}
alignItems={'center'}
h={8}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
>
<MyIcon name={'common/errorFill'} w={'14px'} mr={1} />
<Box color={'red.600'}>
{node?.pluginData?.error || t('app:app.modules.not_found')}
</Box>
</Flex>
</MyTooltip>
)}
</Flex>
<NodeIntro nodeId={nodeId} intro={intro} />

View File

@@ -3,6 +3,7 @@ import {
type ReactNode,
type SetStateAction,
useCallback,
useEffect,
useMemo,
useState
} from 'react';
@@ -21,6 +22,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { checkAppUnExistError } from '@fastgpt/global/core/app/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
const InfoModal = dynamic(() => import('./InfoModal'));
@@ -203,6 +205,16 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
[appDetail.name, deleteApp, openConfirmDel, t]
);
// check app unExist error
useEffect(() => {
if (appDetail.modules.some((module) => checkAppUnExistError(module.pluginData?.error))) {
toast({
title: t('app:app.error.unExist_app'),
status: 'error'
});
}
}, [appDetail.modules, t, toast]);
const contextValue: AppContextType = useMemo(
() => ({
appId,

View File

@@ -46,7 +46,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name);
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar);
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const concatHistory = useMemo(() => {
const formatHistories: HistoryItemType[] = histories.map((item) => {
@@ -146,7 +146,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
overflow={'hidden'}
onClick={() => {
onChangeChatId();
setCiteModalData(undefined);
setQuoteData(undefined);
}}
>
{t('common:core.chat.New Chat')}
@@ -202,7 +202,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
: {
onClick: () => {
onChangeChatId(item.id);
setCiteModalData(undefined);
setQuoteData(undefined);
}
})}
{...(i !== concatHistory.length - 1 && {
@@ -274,7 +274,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
onDelHistory(item.id);
if (item.id === activeChatId) {
onChangeChatId();
setCiteModalData(undefined);
setQuoteData(undefined);
}
},
type: 'danger'

View File

@@ -20,16 +20,17 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { AppListContext } from './context';
import { useContextSelector } from 'use-context-selector';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import type { getMCPToolsBody } from '@/pages/api/support/mcp/client/getTools';
export type MCPToolSetData = {
url: string;
toolList: McpToolConfigType[];
toolList: ToolType[];
};
export type EditMCPToolsProps = {
@@ -40,6 +41,7 @@ export type EditMCPToolsProps = {
const MCPToolsEditModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
@@ -79,7 +81,7 @@ const MCPToolsEditModal = ({ onClose }: { onClose: () => void }) => {
const { runAsync: runGetMCPTools, loading: isGettingTools } = useRequest2(
(data: getMCPToolsBody) => getMCPTools(data),
{
onSuccess: (res: McpToolConfigType[]) => {
onSuccess: (res: ToolType[]) => {
setValue('mcpData.toolList', res);
},
errorToast: t('app:MCP_tools_parse_failed')

View File

@@ -52,7 +52,7 @@ const ApiDatasetForm = ({
{ setTrue: openBaseurlSeletModal, setFalse: closeBaseurlSelectModal }
] = useBoolean();
const parentId = yuqueServer?.basePath || apiServer?.basePath;
const parentId = yuqueServer?.basePath || feishuServer?.folderToken || apiServer?.basePath;
const canSelectBaseUrl = useMemo(() => {
switch (type) {
@@ -61,27 +61,23 @@ const ApiDatasetForm = ({
case DatasetTypeEnum.feishu:
return feishuServer?.appId && feishuServer?.appSecret;
case DatasetTypeEnum.apiDataset:
return !!apiServer?.baseUrl;
return !!apiServer?.basePath;
default:
return false;
}
}, [
type,
yuqueServer?.userId,
yuqueServer?.token,
yuqueServer?.userId,
feishuServer?.appId,
feishuServer?.appSecret,
apiServer?.baseUrl
apiServer?.basePath
]);
// Unified function to get the current path
const { loading: isFetching } = useRequest2(
async () => {
if (
!datasetId &&
((yuqueServer && (!yuqueServer.userId || !yuqueServer?.token)) ||
(apiServer && !apiServer?.baseUrl))
) {
if (!datasetId && !(yuqueServer?.userId && yuqueServer?.token)) {
return setPathNames(t('dataset:input_required_field_to_select_baseurl'));
}
if (!parentId) {
@@ -145,7 +141,7 @@ const ApiDatasetForm = ({
const renderDirectoryModal = () =>
isOpenBaseurlSeletModal ? (
<BaseUrlSelector
selectId={yuqueServer?.basePath || apiServer?.basePath || 'root'}
selectId={type === DatasetTypeEnum.yuque ? yuqueServer?.basePath || 'root' : 'root'}
server={async (e: GetResourceFolderListProps) => {
const params: GetApiDatasetCataLogProps = { parentId: e.parentId };
@@ -207,8 +203,8 @@ const ApiDatasetForm = ({
{...register('apiServer.authorization')}
/>
</Flex>
{renderBaseUrlSelector()}
{renderDirectoryModal()}
{/* {renderBaseUrlSelector()}
{renderDirectoryModal()} */}
</>
)}
{type === DatasetTypeEnum.feishu && (

View File

@@ -148,7 +148,7 @@ const InputDataModal = ({
q: e.q,
a: currentTab === TabEnum.qa ? e.a : '',
// Contains no default index
indexes: e.indexes?.filter((item) => !!item.text?.trim())
indexes: e.indexes.filter((item) => !!item.text?.trim())
});
return {

View File

@@ -160,7 +160,7 @@ const Standard = ({
<Box fontSize={['32px', '42px']} fontWeight={'bold'} color={'myGray.900'}>
{item.price}
</Box>
<Box color={'myGray.500'} minH={'40px'} fontSize={'xs'}>
<Box color={'myGray.500'} h={'40px'} fontSize={'xs'}>
{t(item.desc as any, { title: feConfigs?.systemTitle })}
</Box>
@@ -183,6 +183,17 @@ const Standard = ({
</Button>
);
}
// feature:
// if (
// item.level === myStandardPlan?.nextSubLevel &&
// selectSubMode === myStandardPlan?.nextMode
// ) {
// return (
// <Button mt={4} mb={6} w={'100%'} variant={'whiteBase'} isDisabled>
// {t('common:support.wallet.subscription.Next plan')}
// </Button>
// );
// }
if (isCurrentPlan) {
return (
<Button
@@ -301,7 +312,7 @@ const RowTabs = ({
px={'12px'}
py={'7px'}
userSelect={'none'}
w={['150px', '190px']}
w={['150px', '170px']}
{...(value === item.value
? {
color: 'white',

View File

@@ -81,5 +81,4 @@ function App({ Component, pageProps }: AppPropsWithLayout) {
);
}
// @ts-ignore
export default appWithTranslation(App);

View File

@@ -4,7 +4,7 @@ import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/u
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { type CreateAppBody, onCreateApp } from '../create';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import {
@@ -13,7 +13,6 @@ import {
} from '@fastgpt/global/core/app/mcpTools/utils';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { checkTeamAppLimit } from '@fastgpt/service/support/permission/teamLimit';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export type createMCPToolsQuery = {};
@@ -22,7 +21,7 @@ export type createMCPToolsBody = Omit<
'type' | 'modules' | 'edges' | 'chatConfig'
> & {
url: string;
toolList: McpToolConfigType[];
toolList: ToolType[];
};
export type createMCPToolsResponse = {};
@@ -34,7 +33,7 @@ async function handler(
const { name, avatar, toolList, url, parentId } = req.body;
const { teamId, tmbId, userId } = parentId
? await authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true })
? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true })
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal });
await checkTeamAppLimit(teamId);

View File

@@ -1,6 +1,6 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { type AppDetailType, type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type AppDetailType, type ToolType } from '@fastgpt/global/core/app/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
@@ -24,7 +24,7 @@ export type updateMCPToolsQuery = {};
export type updateMCPToolsBody = {
appId: string;
url: string;
toolList: McpToolConfigType[];
toolList: ToolType[];
};
export type updateMCPToolsResponse = {};
@@ -56,11 +56,10 @@ async function handler(
});
}
await MongoApp.updateOne(
{ _id: appId },
await MongoApp.findByIdAndUpdate(
appId,
{
modules: [getMCPToolSetRuntimeNode({ url, toolList, name: app.name, avatar: app.avatar })],
updateTime: new Date()
modules: [getMCPToolSetRuntimeNode({ url, toolList, name: app.name, avatar: app.avatar })]
},
{ session }
);
@@ -98,7 +97,6 @@ const updateMCPChildrenTool = async ({
teamId
});
// 删除 DB 里有,新的工具列表里没有的工具
for await (const tool of dbTools) {
if (!toolSetData.toolList.find((t) => t.name === tool.name)) {
await onDelOneApp({
@@ -109,7 +107,6 @@ const updateMCPChildrenTool = async ({
}
}
// 创建 DB 里没有,新的工具列表里有的工具
for await (const tool of toolSetData.toolList) {
if (!dbTools.find((t) => t.name === tool.name)) {
await onCreateApp({
@@ -126,12 +123,11 @@ const updateMCPChildrenTool = async ({
}
}
// 更新 DB 里有的工具
for await (const tool of toolSetData.toolList) {
const dbTool = dbTools.find((t) => t.name === tool.name);
if (dbTool) {
await MongoApp.updateOne(
{ _id: dbTool._id },
await MongoApp.findByIdAndUpdate(
dbTool._id,
{
modules: [getMCPToolRuntimeNode({ tool, url: toolSetData.url })]
},

View File

@@ -27,7 +27,7 @@ async function handler(
const match = {
appId,
...(isPublish !== undefined && { isPublish })
isPublish
};
const [result, total] = await Promise.all([

View File

@@ -55,14 +55,12 @@ async function handler(
const isPlugin = app.type === AppTypeEnum.plugin;
const isOutLink = authType === GetChatTypeEnum.outLink;
const commonField =
'dataId obj value adminFeedback userGoodFeedback userBadFeedback time hideInUI durationSeconds errorMsg';
const fieldMap = {
[GetChatTypeEnum.normal]: `${commonField} ${
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback time hideInUI durationSeconds ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`,
[GetChatTypeEnum.outLink]: `${commonField} ${DispatchNodeResponseKeyEnum.nodeResponse}`,
[GetChatTypeEnum.team]: `${commonField} ${DispatchNodeResponseKeyEnum.nodeResponse}`
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time hideInUI durationSeconds ${DispatchNodeResponseKeyEnum.nodeResponse}`,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time hideInUI durationSeconds ${DispatchNodeResponseKeyEnum.nodeResponse}`
};
const { total, histories } = await getChatItems({

View File

@@ -6,7 +6,7 @@ import { quoteDataFieldSelector, type QuoteDataItemType } from '@/service/core/c
import { processChatTimeFilter } from '@/service/core/chat/utils';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
export type GetQuoteProps = {
export type GetQuoteDataProps = {
datasetDataIdList: string[];
collectionIdList: string[];
@@ -19,9 +19,9 @@ export type GetQuoteProps = {
teamToken?: string;
};
export type GetQuotesRes = QuoteDataItemType[];
export type GetQuoteDataRes = QuoteDataItemType[];
async function handler(req: ApiRequestProps<GetQuoteProps>): Promise<GetQuotesRes> {
async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuoteDataRes> {
const {
appId,
chatId,

View File

@@ -4,15 +4,13 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import type {
APIFileServer,
YuqueServer,
FeishuServer,
ApiDatasetDetailResponse
FeishuServer
} from '@fastgpt/global/core/dataset/apiDataset';
import { getProApiDatasetFileDetailRequest } from '@/service/core/dataset/apiDataset/controller';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
export type GetApiDatasetPathQuery = {};
@@ -26,24 +24,6 @@ export type GetApiDatasetPathBody = {
export type GetApiDatasetPathResponse = string;
const getFullPath = async (
currentId: string,
getFileDetail: ({ apiFileId }: { apiFileId: string }) => Promise<ApiDatasetDetailResponse>
): Promise<string> => {
const response = await getFileDetail({ apiFileId: currentId });
if (!response) {
return '';
}
if (response.parentId && response.parentId !== null) {
const parentPath = await getFullPath(response.parentId, getFileDetail);
return `${parentPath}/${response.name}`;
}
return `/${response.name}`;
};
async function handler(
req: ApiRequestProps<GetApiDatasetPathBody, any>,
res: ApiResponseType<GetApiDatasetPathResponse>
@@ -90,22 +70,27 @@ async function handler(
return Promise.reject(DatasetErrEnum.noApiServer);
}
if (feishuServer) {
return '';
}
if (apiServer) {
return await getFullPath(parentId, useApiDatasetRequest({ apiServer }).getFileDetail);
if (apiServer || feishuServer) {
return Promise.reject('不支持获取 BaseUrl');
}
if (yuqueServer) {
const yuqueFileGetter = async ({ apiFileId }: { apiFileId: string }) => {
return await getProApiDatasetFileDetailRequest({
const getFullPath = async (currentId: string): Promise<string> => {
const response = await getProApiDatasetFileDetailRequest({
feishuServer,
yuqueServer,
apiFileId
apiFileId: currentId
});
if (response.parentId) {
const parentPath = await getFullPath(response.parentId);
return `${parentPath}/${response.name}`;
}
return `/${response.name}`;
};
return await getFullPath(parentId, yuqueFileGetter);
return await getFullPath(parentId);
}
return Promise.reject(new Error(DatasetErrEnum.noApiServer));

View File

@@ -8,7 +8,6 @@ import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/ch
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { i18nT } from '@fastgpt/web/i18n/utils';
export type GetQuoteDataResponse = {
collection: DatasetCollectionSchemaType;
@@ -47,7 +46,7 @@ async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuot
const datasetData = await MongoDatasetData.findById(dataId);
if (!datasetData) {
return Promise.reject(i18nT('common:data_not_found'));
return Promise.reject('Can not find the data');
}
const [collection, { responseDetail }] = await Promise.all([

View File

@@ -48,8 +48,7 @@ async function handler(req: ApiRequestProps<Query>): Promise<DatasetItemType> {
apiServer: dataset.apiServer
? {
baseUrl: dataset.apiServer.baseUrl,
authorization: '',
basePath: dataset.apiServer.basePath
authorization: ''
}
: undefined,
yuqueServer: dataset.yuqueServer

View File

@@ -170,7 +170,6 @@ async function handler(
...(!!apiServer?.authorization && {
'apiServer.authorization': apiServer.authorization
}),
...(!!apiServer?.basePath !== undefined && { 'apiServer.basePath': apiServer?.basePath }),
...(!!yuqueServer?.userId && { 'yuqueServer.userId': yuqueServer.userId }),
...(!!yuqueServer?.token && { 'yuqueServer.token': yuqueServer.token }),
...(!!yuqueServer?.basePath !== undefined && {

View File

@@ -1,5 +1,5 @@
import { NextAPI } from '@/service/middleware/entry';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { MCPClient } from '@fastgpt/service/core/app/mcp';
@@ -7,7 +7,7 @@ export type getMCPToolsQuery = {};
export type getMCPToolsBody = { url: string };
export type getMCPToolsResponse = McpToolConfigType[];
export type getMCPToolsResponse = ToolType[];
async function handler(
req: ApiRequestProps<getMCPToolsBody, getMCPToolsQuery>,

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { setCookie } from '@fastgpt/service/support/permission/controller';
import { createJWT, setCookie } from '@fastgpt/service/support/permission/controller';
import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { PostLoginProps } from '@fastgpt/global/support/user/api.d';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
@@ -13,8 +13,6 @@ import { addOperationLog } from '@fastgpt/service/support/operationLog/addOperat
import { OperationLogEventEnum } from '@fastgpt/global/support/operationLog/constants';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { authCode } from '@fastgpt/service/support/user/auth/controller';
import { createUserSession } from '@fastgpt/service/support/user/session';
import requestIp from 'request-ip';
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { username, password, code } = req.body as PostLoginProps;
@@ -63,22 +61,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
lastLoginTmbId: userDetail.team.tmbId
});
const token = await createUserSession({
userId: user._id,
teamId: userDetail.team.teamId,
tmbId: userDetail.team.tmbId,
isRoot: username === 'root',
ip: requestIp.getClientIp(req)
});
setCookie(res, token);
pushTrack.login({
type: 'password',
uid: user._id,
teamId: userDetail.team.teamId,
tmbId: userDetail.team.tmbId
});
const token = createJWT({
...userDetail,
isRoot: username === 'root'
});
setCookie(res, token);
addOperationLog({
tmbId: userDetail.team.tmbId,
teamId: userDetail.team.teamId,

View File

@@ -59,8 +59,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@@ -148,7 +148,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
);
return isPc || !appId ? (
<SideBar externalTrigger={!!datasetCiteData}>{Children}</SideBar>
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
) : (
<Drawer
isOpen={isOpenSlider}
@@ -161,7 +161,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
</Drawer>
);
}, [t, isPc, appId, isOpenSlider, onCloseSlider, datasetCiteData]);
}, [t, isPc, appId, isOpenSlider, onCloseSlider, quoteData]);
return (
<Flex h={'100%'}>
@@ -173,7 +173,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
</Box>
)}
{(!datasetCiteData || isPc) && (
{(!quoteData || isPc) && (
<PageContainer flex={'1 0 0'} w={0} p={[0, '16px']} position={'relative'}>
<Flex h={'100%'} flexDirection={['column', 'row']}>
{/* pc always show history. */}
@@ -222,12 +222,12 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
</PageContainer>
)}
{datasetCiteData && (
{quoteData && (
<PageContainer flex={'1 0 0'} w={0} maxW={'560px'}>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</PageContainer>
)}

View File

@@ -85,8 +85,8 @@ const OutLink = (props: Props) => {
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const isResponseDetail = useContextSelector(ChatItemContext, (v) => v.isResponseDetail);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
@@ -226,7 +226,7 @@ const OutLink = (props: Props) => {
if (showHistory !== '1') return null;
return isPc ? (
<SideBar externalTrigger={!!datasetCiteData}>{Children}</SideBar>
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
) : (
<Drawer
isOpen={isOpenSlider}
@@ -241,7 +241,7 @@ const OutLink = (props: Props) => {
</DrawerContent>
</Drawer>
);
}, [isOpenSlider, isPc, onCloseSlider, datasetCiteData, showHistory, t]);
}, [isOpenSlider, isPc, onCloseSlider, quoteData, showHistory, t]);
return (
<>
@@ -255,7 +255,7 @@ const OutLink = (props: Props) => {
gap={4}
{...(isEmbed ? { p: '0 !important', borderRadius: '0', boxShadow: 'none' } : { p: [0, 5] })}
>
{(!datasetCiteData || isPc) && (
{(!quoteData || isPc) && (
<PageContainer flex={'1 0 0'} w={0} p={'0 !important'}>
<Flex h={'100%'} flexDirection={['column', 'row']}>
{RenderHistoryList}
@@ -303,12 +303,12 @@ const OutLink = (props: Props) => {
</PageContainer>
)}
{datasetCiteData && (
{quoteData && (
<PageContainer flex={'1 0 0'} w={0} maxW={'560px'} p={'0 !important'}>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</PageContainer>
)}

View File

@@ -64,8 +64,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@@ -166,7 +166,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
);
return isPc || !appId ? (
<SideBar externalTrigger={!!datasetCiteData}>{Children}</SideBar>
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
) : (
<Drawer
isOpen={isOpenSlider}
@@ -179,7 +179,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
</Drawer>
);
}, [appId, isOpenSlider, isPc, onCloseSlider, datasetCiteData, t]);
}, [appId, isOpenSlider, isPc, onCloseSlider, quoteData, t]);
return (
<Flex h={'100%'}>
@@ -191,7 +191,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
</Box>
)}
{(!datasetCiteData || isPc) && (
{(!quoteData || isPc) && (
<PageContainer flex={'1 0 0'} w={0} p={[0, '16px']} position={'relative'}>
<Flex h={'100%'} flexDirection={['column', 'row']} bg={'white'}>
{RenderHistoryList}
@@ -236,12 +236,12 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
</Flex>
</PageContainer>
)}
{datasetCiteData && (
{quoteData && (
<PageContainer flex={'1 0 0'} w={0} maxW={'560px'}>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
onClose={() => setCiteModalData(undefined)}
rawSearch={quoteData.rawSearch}
metadata={quoteData.metadata}
onClose={() => setQuoteData(undefined)}
/>
</PageContainer>
)}

View File

@@ -262,7 +262,7 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
{/* Folder slider */}
{!!folderDetail && isPc && (
<Box pt={[4, 6]} pr={[4, 6]} h={'100%'} pb={4} overflow={'auto'}>
<Box pt={[4, 6]} pr={[4, 6]}>
<FolderSlideCard
refetchResource={() => Promise.all([refetchFolderDetail(), loadMyApps()])}
resumeInheritPermission={() => resumeInheritPer(folderDetail._id)}

View File

@@ -30,7 +30,6 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import dynamic from 'next/dynamic';
import { type McpKeyType } from '@fastgpt/global/support/mcp/type';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useUserStore } from '@/web/support/user/useUserStore';
const UsageWay = dynamic(() => import('@/pageComponents/dashboard/mcp/usageWay'), {
ssr: false
@@ -39,7 +38,6 @@ const UsageWay = dynamic(() => import('@/pageComponents/dashboard/mcp/usageWay')
const McpServer = () => {
const { t } = useTranslation();
const { isPc } = useSystem();
const { userInfo } = useUserStore();
const {
data: mcpServerList = [],
@@ -80,10 +78,7 @@ const McpServer = () => {
{t('dashboard_mcp:mcp_server_description')}
</Box>
</Box>
<Button
isDisabled={!userInfo?.permission.hasApikeyCreatePer}
onClick={() => setEditMcp(defaultForm)}
>
<Button onClick={() => setEditMcp(defaultForm)}>
{t('dashboard_mcp:create_mcp_server')}
</Button>
</Flex>
@@ -99,10 +94,7 @@ const McpServer = () => {
{t('dashboard_mcp:mcp_server_description')}
</Box>
<Flex mt={2} justifyContent={'flex-end'}>
<Button
isDisabled={!userInfo?.permission.hasApikeyCreatePer}
onClick={() => setEditMcp(defaultForm)}
>
<Button onClick={() => setEditMcp(defaultForm)}>
{t('dashboard_mcp:create_mcp_server')}
</Button>
</Flex>

View File

@@ -223,7 +223,7 @@ const Dataset = () => {
</Flex>
{!!folderDetail && isPc && (
<Box ml="6" h={'100%'} pb={4} overflow={'auto'}>
<Box ml="6">
<FolderSlideCard
resumeInheritPermission={() => resumeInheritPer(folderDetail._id)}
isInheritPermission={folderDetail.inheritPermission}

View File

@@ -29,8 +29,10 @@ import {
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { type StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getErrText } from '@fastgpt/global/common/error/utils';
import {
type PluginDataType,
type StoreNodeItemType
} from '@fastgpt/global/core/workflow/type/node';
export const getScheduleTriggerApp = async () => {
// 1. Find all the app
@@ -150,7 +152,6 @@ export const checkNode = async ({
try {
const { source } = await splitCombinePluginId(pluginId);
if (source === PluginSourceEnum.personal) {
await authAppByTmbId({
tmbId: ownerTmbId,
@@ -175,8 +176,8 @@ export const checkNode = async ({
return {
...node,
pluginData: {
error: getErrText(error)
}
error
} as PluginDataType
};
}
};

View File

@@ -109,12 +109,6 @@ function checkRes(data: ResponseDataType) {
*/
function responseError(err: any) {
console.log('error->', '请求错误', err);
const isOutlinkPage = {
'/chat/share': true,
'/chat/team': true,
'/login': true
}[window.location.pathname];
const data = err?.response?.data || err;
if (!err) {
@@ -129,7 +123,7 @@ function responseError(err: any) {
// 有报错响应
if (data?.code in TOKEN_ERROR_CODE) {
if (!isOutlinkPage) {
if (!['/chat/share', '/chat/team', '/login'].includes(window.location.pathname)) {
clearToken();
window.location.replace(
getWebReqUrl(`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`)
@@ -139,17 +133,13 @@ function responseError(err: any) {
return Promise.reject({ message: i18nT('common:unauth_token') });
}
if (
data?.statusText &&
[
TeamErrEnum.aiPointsNotEnough,
TeamErrEnum.datasetSizeNotEnough,
TeamErrEnum.datasetAmountNotEnough,
TeamErrEnum.appAmountNotEnough,
TeamErrEnum.pluginAmountNotEnough,
TeamErrEnum.websiteSyncNotEnough,
TeamErrEnum.reRankNotEnough
].includes(data?.statusText) &&
!isOutlinkPage
data?.statusText === TeamErrEnum.aiPointsNotEnough ||
data?.statusText === TeamErrEnum.datasetSizeNotEnough ||
data?.statusText === TeamErrEnum.datasetAmountNotEnough ||
data?.statusText === TeamErrEnum.appAmountNotEnough ||
data?.statusText === TeamErrEnum.pluginAmountNotEnough ||
data?.statusText === TeamErrEnum.websiteSyncNotEnough ||
data?.statusText === TeamErrEnum.reRankNotEnough
) {
useSystemStore.getState().setNotSufficientModalType(data.statusText);
return Promise.reject(data);

View File

@@ -20,7 +20,7 @@ import type { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/typ
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { defaultGroup } from '@fastgpt/web/core/workflow/constants';
import type { createMCPToolsBody } from '@/pages/api/core/app/mcpTools/create';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type ToolType } from '@fastgpt/global/core/app/type';
import type { updateMCPToolsBody } from '@/pages/api/core/app/mcpTools/update';
import type { RunMCPToolBody } from '@/pages/api/support/mcp/client/runTool';
import type { getMCPToolsBody } from '@/pages/api/support/mcp/client/getTools';
@@ -79,7 +79,7 @@ export const postUpdateMCPTools = (data: updateMCPToolsBody) =>
POST('/core/app/mcpTools/update', data);
export const getMCPTools = (data: getMCPToolsBody) =>
POST<McpToolConfigType[]>('/support/mcp/client/getTools', data);
POST<ToolType[]>('/support/mcp/client/getTools', data);
export const postRunMCPTool = (data: RunMCPToolBody) => POST('/support/mcp/client/runTool', data);

View File

@@ -25,7 +25,7 @@ import type {
getPaginationRecordsBody,
getPaginationRecordsResponse
} from '@/pages/api/core/chat/getPaginationRecords';
import type { GetQuoteProps, GetQuotesRes } from '@/pages/api/core/chat/quote/getQuote';
import type { GetQuoteDataProps, GetQuoteDataRes } from '@/pages/api/core/chat/quote/getQuote';
import type {
GetCollectionQuoteProps,
GetCollectionQuoteRes
@@ -101,8 +101,8 @@ export const getMyTokensApps = (data: AuthTeamTagTokenProps) =>
export const getinitTeamChat = (data: { teamId: string; authToken: string; appId: string }) =>
GET(`/proApi/core/chat/initTeamChat`, data);
export const getQuoteDataList = (data: GetQuoteProps) =>
POST<GetQuotesRes>(`/core/chat/quote/getQuote`, data);
export const getQuoteDataList = (data: GetQuoteDataProps) =>
POST<GetQuoteDataRes>(`/core/chat/quote/getQuote`, data);
export const getCollectionQuote = (data: GetCollectionQuoteProps) =>
POST<GetCollectionQuoteRes>(`/core/chat/quote/getCollectionQuote`, data);

View File

@@ -34,13 +34,13 @@ type ChatBoxDataType = {
};
};
// 知识库引用相关 type
export type GetQuoteDataBasicProps = {
appId: string;
chatId: string;
chatItemDataId: string;
outLinkAuthData?: OutLinkChatAuthProps;
};
// 获取单个集合引用
export type GetCollectionQuoteDataProps = GetQuoteDataBasicProps & {
quoteId?: string;
collectionId: string;
@@ -54,17 +54,11 @@ export type GetAllQuoteDataProps = GetQuoteDataBasicProps & {
sourceName?: string;
};
export type GetQuoteProps = GetAllQuoteDataProps | GetCollectionQuoteDataProps;
export type QuoteDataType = {
rawSearch: SearchDataResponseItemType[];
metadata: GetQuoteProps;
};
export type OnOpenCiteModalProps = {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
quoteId?: string;
};
type ChatItemContextType = {
ChatBoxRef: React.RefObject<ChatComponentRef> | null;
@@ -80,8 +74,8 @@ type ChatItemContextType = {
setChatBoxData: React.Dispatch<React.SetStateAction<ChatBoxDataType>>;
isPlugin: boolean;
datasetCiteData?: QuoteDataType;
setCiteModalData: React.Dispatch<React.SetStateAction<QuoteDataType | undefined>>;
quoteData?: QuoteDataType;
setQuoteData: React.Dispatch<React.SetStateAction<QuoteDataType | undefined>>;
isVariableVisible: boolean;
setIsVariableVisible: React.Dispatch<React.SetStateAction<boolean>>;
} & ContextProps;
@@ -104,8 +98,8 @@ export const ChatItemContext = createContext<ChatItemContextType>({
throw new Error('Function not implemented.');
},
datasetCiteData: undefined,
setCiteModalData: function (value: React.SetStateAction<QuoteDataType | undefined>): void {
quoteData: undefined,
setQuoteData: function (value: React.SetStateAction<QuoteDataType | undefined>): void {
throw new Error('Function not implemented.');
},
isVariableVisible: true,
@@ -130,6 +124,7 @@ const ChatItemContextProvider = ({
} & ContextProps) => {
const ChatBoxRef = useRef<ChatComponentRef>(null);
const variablesForm = useForm<ChatBoxInputFormType>();
const [quoteData, setQuoteData] = useState<QuoteDataType>();
const [isVariableVisible, setIsVariableVisible] = useState(true);
const [chatBoxData, setChatBoxData] = useState<ChatBoxDataType>({
@@ -167,8 +162,6 @@ const ChatItemContextProvider = ({
ChatBoxRef.current?.restartChat?.();
}, [variablesForm]);
const [datasetCiteData, setCiteModalData] = useState<QuoteDataType>();
const contextValue = useMemo(() => {
return {
chatBoxData,
@@ -187,8 +180,8 @@ const ChatItemContextProvider = ({
// isShowFullText,
showNodeStatus,
datasetCiteData,
setCiteModalData,
quoteData,
setQuoteData,
isVariableVisible,
setIsVariableVisible
};
@@ -205,8 +198,8 @@ const ChatItemContextProvider = ({
isResponseDetail,
// isShowFullText,
showNodeStatus,
datasetCiteData,
setCiteModalData,
quoteData,
setQuoteData,
isVariableVisible,
setIsVariableVisible
]);

View File

@@ -23,13 +23,25 @@ type State = {
const createCustomStorage = () => {
const sessionKeys = ['source', 'chatId', 'appId'];
// 从 URL 中获取 appId 作为存储键的一部分
const getStorageKey = (name: string) => {
let appId = '';
if (typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search);
appId = urlParams.get('appId') || '';
}
return appId ? `${name}_${appId}` : name;
};
return {
getItem: (name: string) => {
const sessionData = JSON.parse(sessionStorage.getItem(name) || '{}');
const localData = JSON.parse(localStorage.getItem(name) || '{}');
const storageKey = getStorageKey(name);
const sessionData = JSON.parse(sessionStorage.getItem(storageKey) || '{}');
const localData = JSON.parse(localStorage.getItem(storageKey) || '{}');
return JSON.stringify({ ...localData, ...sessionData });
},
setItem: (name: string, value: string) => {
const storageKey = getStorageKey(name);
const data = JSON.parse(value);
// 分离 session 和 local 数据
@@ -42,15 +54,16 @@ const createCustomStorage = () => {
// 分别存储
if (Object.keys(sessionData).length > 0) {
sessionStorage.setItem(name, JSON.stringify({ state: sessionData, version: 0 }));
sessionStorage.setItem(storageKey, JSON.stringify({ state: sessionData, version: 0 }));
}
if (Object.keys(localData).length > 0) {
localStorage.setItem(name, JSON.stringify({ state: localData, version: 0 }));
localStorage.setItem(storageKey, JSON.stringify({ state: localData, version: 0 }));
}
},
removeItem: (name: string) => {
sessionStorage.removeItem(name);
localStorage.removeItem(name);
const storageKey = getStorageKey(name);
sessionStorage.removeItem(storageKey);
localStorage.removeItem(storageKey);
}
};
};
@@ -125,3 +138,5 @@ export const useChatStore = create<State>()(
)
)
);
export { createCustomStorage };

View File

@@ -72,7 +72,7 @@ import type {
getTrainingErrorResponse
} from '@/pages/api/core/dataset/training/getTrainingError';
import type { APIFileItem } from '@fastgpt/global/core/dataset/apiDataset';
import type { GetQuoteDataProps } from '@/pages/api/core/dataset/data/getQuoteData';
import type { GetQuoteDataProps } from '@/pages/api/core/chat/quote/getQuote';
import type {
GetApiDatasetCataLogResponse,
GetApiDatasetCataLogProps

View File

@@ -106,8 +106,7 @@ export const DatasetPageContextProvider = ({
apiServer: data.apiServer
? {
baseUrl: data.apiServer.baseUrl,
authorization: '',
basePath: data.apiServer.basePath
authorization: ''
}
: state.apiServer,
yuqueServer: data.yuqueServer

View File

@@ -384,22 +384,6 @@ describe('Parse dataset cite content test', async () => {
content: '知识库问答系统[67e517e7476861](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统[67e517e7476861](CITE)'
}
},
{
// [id](CITE)
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[i' },
{ content: 'd](CITE)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[id](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统'
}
}
];

View File

@@ -0,0 +1,170 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { useChatStore, createCustomStorage } from '@/web/core/chat/context/useChatStore';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
vi.mock('@fastgpt/global/common/string/tools', () => ({
getNanoid: vi.fn().mockReturnValue('test-nanoid')
}));
const mockStorage = () => {
const store = new Map();
return {
getItem: (key: string) => store.get(key) || null,
setItem: (key: string, value: string) => store.set(key, value),
clear: () => store.clear(),
removeItem: (key: string) => store.delete(key)
};
};
const mockWindow = () => {
const windowMock = {
location: {
search: '?appId=test123'
},
sessionStorage: mockStorage(),
localStorage: mockStorage()
};
vi.stubGlobal('window', windowMock);
global.sessionStorage = windowMock.sessionStorage;
global.localStorage = windowMock.localStorage;
};
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mockWindow();
const store = useChatStore.getState();
store.source = undefined;
store.appId = '';
store.chatId = '';
store.lastChatId = '';
store.lastChatAppId = '';
store.outLinkAuthData = {};
sessionStorage.clear();
localStorage.clear();
});
describe('useChatStore', () => {
it('should set source and restore last chat if available', () => {
const store = useChatStore.getState();
store.lastChatAppId = 'app123';
store.lastChatId = `${ChatSourceEnum.share}-chat123`;
store.setSource(ChatSourceEnum.share);
const updatedStore = useChatStore.getState();
expect(updatedStore.source).toBe(ChatSourceEnum.share);
expect(updatedStore.chatId).toBe('chat123');
expect(updatedStore.lastChatAppId).toBe('app123');
});
it('should generate new chatId when source changes', () => {
const store = useChatStore.getState();
store.source = ChatSourceEnum.share;
store.chatId = 'old-id';
store.setSource(ChatSourceEnum.api);
const updatedStore = useChatStore.getState();
expect(updatedStore.chatId).toBe('test-nanoid');
expect(updatedStore.chatId).not.toBe('old-id');
});
it('should set appId and lastChatAppId', () => {
const store = useChatStore.getState();
store.setAppId('test123');
const updatedStore = useChatStore.getState();
expect(updatedStore.appId).toBe('test123');
expect(updatedStore.lastChatAppId).toBe('test123');
});
it('should not set empty appId', () => {
const store = useChatStore.getState();
store.setAppId('test123');
store.setAppId('');
const updatedStore = useChatStore.getState();
expect(updatedStore.appId).toBe('test123');
expect(updatedStore.lastChatAppId).toBe('test123');
});
it('should set chatId and lastChatId', () => {
const store = useChatStore.getState();
store.source = ChatSourceEnum.share;
store.setChatId('test-id');
const updatedStore = useChatStore.getState();
expect(updatedStore.chatId).toBe('test-id');
expect(updatedStore.lastChatId).toBe(`${ChatSourceEnum.share}-test-id`);
});
it('should generate new chatId if none provided', () => {
const store = useChatStore.getState();
store.source = ChatSourceEnum.share;
store.setChatId();
const updatedStore = useChatStore.getState();
expect(updatedStore.chatId).toBe('test-nanoid');
});
it('should set outLinkAuthData', () => {
const store = useChatStore.getState();
const authData = { apikey: 'test-key' };
store.setOutLinkAuthData(authData);
const updatedStore = useChatStore.getState();
expect(updatedStore.outLinkAuthData).toEqual(authData);
});
});
describe('createCustomStorage', () => {
it('should create storage with appId in key', () => {
const storage = createCustomStorage();
const testData = {
state: {
source: ChatSourceEnum.share,
chatId: '123',
appId: 'app123',
lastChatId: 'last123',
lastChatAppId: 'lastApp123'
},
version: 0
};
storage.setItem('test', JSON.stringify(testData));
const sessionResult = JSON.parse(sessionStorage.getItem('test_test123') || '{}');
const localResult = JSON.parse(localStorage.getItem('test_test123') || '{}');
expect(sessionResult.state).toEqual({
source: ChatSourceEnum.share,
chatId: '123',
appId: 'app123'
});
expect(localResult.state).toEqual({
lastChatId: 'last123',
lastChatAppId: 'lastApp123'
});
});
it('should remove items from both storages', () => {
const storage = createCustomStorage();
const testData = {
state: {
source: ChatSourceEnum.share,
chatId: '123'
},
version: 0
};
storage.setItem('test', JSON.stringify(testData));
storage.removeItem('test');
expect(sessionStorage.getItem('test_test123')).toBeNull();
expect(localStorage.getItem('test_test123')).toBeNull();
});
});

View File

@@ -0,0 +1 @@
{"/root/.cache/node/corepack/v1/pnpm/9.15.5/bin/pnpm.cjs":["f5286335ac1397138eff043e0d78e29501577055",0,1472],"/root/.cache/node/corepack/v1/pnpm/9.15.5/dist/pnpm.cjs":["0b2b22796df6e249cd89f4cdf06786d40a52564e",1472,886352]}