4.6.4-alpha (#569)

This commit is contained in:
Archer 2023-12-07 13:43:08 +08:00 committed by GitHub
parent 71afe71192
commit e01c38efe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1401 additions and 1109 deletions

View File

@ -1,94 +1,175 @@
--- ---
title: '分享链接鉴权' title: '分享链接身份鉴权'
description: 'FastGPT 分享链接鉴权' description: 'FastGPT 分享链接身份鉴权'
icon: 'share' icon: 'share'
draft: false draft: false
toc: true toc: true
weight: 860 weight: 860
--- ---
## 介绍
在 FastGPT V4.6.4 中,我们修改了分享链接的数据读取方式,为每个用户生成一个 localId用于标识用户从云端拉取对话记录。但是这种方式仅能保障用户在同一设备同一浏览器中使用如果切换设备或者清空浏览器缓存则会丢失这些记录。这种方式存在一定的风险因此我们仅允许用户拉取近`30天``20条`记录。
分享链接身份鉴权设计的目的在于,将 FastGPT 的对话框快速、安全的接入到你现有的系统中,仅需 2 个接口即可实现。
## 使用说明 ## 使用说明
分享链接鉴权设计的目的在于,将 FastGPT 的对话框安全的接入你现有的系统中。 免登录链接配置中,你可以选择填写`身份验证`栏。这是一个`POST`请求的根地址。在填写该地址后,分享链接的初始化、开始对话以及对话结束都会向该地址的特定接口发送一条请求。下面以`host`来表示`凭身份验证根地址`。服务器接口仅需返回是否校验成功即可,不需要返回其他数据,格式如下:
免登录链接配置中,增加了`凭证校验服务器`后,使用分享链接时会向服务器发起请求,校验链接是否可用,并在每次对话结束后,向服务器发送对话结果。下面以`host`来表示`凭证校验服务器`。服务器接口仅需返回是否校验成功即可,不需要返回其他数据,格式如下: ### 接口统一响应格式
```json ```json
{ {
"success": true, "success": true,
"message": "错误提示", "message": "错误提示",
"msg": "同message, 错误提示" "msg": "同message, 错误提示",
"uid": "用户唯一凭证"
} }
``` ```
`FastGPT` 将会判断`success`是否为`true`决定是允许用户继续操作。`message``msg`是等同的,你可以选择返回其中一个,当`success`不为`true`时,将会提示这个错误。
`uid`是用户的唯一凭证,将会用于拉取对话记录以及保存对话记录。可参考下方实践案例。
### 触发流程
![](/imgs/sharelinkProcess.png) ![](/imgs/sharelinkProcess.png)
## 配置校验地址和校验token ## 配置教程
### 1. 配置身份校验地址
### 1. 配置校验地址的`BaseURL`
![](/imgs/share-setlink.jpg) ![](/imgs/share-setlink.jpg)
配置校验地址后,在每次分享链接使用时,都会向对应的地址发起校验和上报请求。 配置校验地址后,在每次分享链接使用时,都会向对应的地址发起校验和上报请求。
{{% alert icon="🤖" %}}
这里仅需配置根地址,无需具体到完整请求路径。
{{% /alert %}}
### 2. 分享链接中增加额外 query ### 2. 分享链接中增加额外 query
在分享链接的地址中,增加一个额外的参数: authToken。例如 在分享链接的地址中,增加一个额外的参数: authToken。例如
原始的链接https://fastgpt.run/chat/share?shareId=648aaf5ae121349a16d62192 原始的链接:`https://fastgpt.run/chat/share?shareId=648aaf5ae121349a16d62192`
完整链接: https://fastgpt.run/chat/share?shareId=648aaf5ae121349a16d62192&authToken=userid12345
这个`token`通常是你系统生成的在发出校验请求时FastGPT 会在`body`中携带 token={{authToken}} 的参数。 完整链接: `https://fastgpt.run/chat/share?shareId=648aaf5ae121349a16d62192&authToken=userid12345`
## 聊天初始化校验 这个`authToken`通常是你系统生成的用户唯一凭证Token之类的。FastGPT 会在鉴权接口的`body`中携带 token={{authToken}} 的参数。
**FastGPT 发出的请求** ### 3. 编写聊天初始化校验接口
{{< tabs tabTotal="3" >}}
{{< tab tabName="请求示例" >}}
{{< markdownify >}}
```bash ```bash
curl --location --request POST '{{host}}/shareAuth/init' \ curl --location --request POST '{{host}}/shareAuth/init' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
"token": "sintdolore" "token": "{{authToken}}"
}' }'
``` ```
**响应示例** {{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="鉴权成功" >}}
{{< markdownify >}}
```json
{
"success": true,
"uid": "username123",
}
```
系统会拉取该分享链接下uid 为 username123 的对话记录。
{{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="鉴权失败" >}}
{{< markdownify >}}
```json ```json
{ {
"success": false, "success": false,
"message": "分享链接无效", "message": "身份错误",
} }
``` ```
## 对话前校验 {{< /markdownify >}}
{{< /tab >}}
{{< /tabs >}}
**FastGPT 发出的请求**
### 4. 编写对话前校验接口
{{< tabs tabTotal="3" >}}
{{< tab tabName="请求示例" >}}
{{< markdownify >}}
```bash ```bash
curl --location --request POST '{{host}}/shareAuth/start' \ curl --location --request POST '{{host}}/shareAuth/start' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
"token": "sintdolore", "token": "{{authToken}}",
"question": "用户问题", "question": "用户问题",
}' }'
``` ```
**响应示例** {{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="鉴权成功" >}}
{{< markdownify >}}
```json ```json
{ {
"success": true "success": true,
"uid": "username123",
} }
``` ```
## 对话结果上报 {{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="鉴权失败" >}}
{{< markdownify >}}
```json
{
"success": false,
"message": "身份验证失败",
}
```
```json
{
"success": false,
"message": "存在违规词",
}
```
{{< /markdownify >}}
{{< /tab >}}
{{< /tabs >}}
### 5. 编写对话结果上报接口(可选)
该接口无规定返回值。
响应值与[chat 接口格式相同](/docs/development/openapi/chat/#响应),仅多了一个`token`
可以重点关注`responseData`里的`price`值,`price`与实际价格的倍率为`100000`,即 100000=1元。
```bash ```bash
curl --location --request POST '{{host}}/shareAuth/finish' \ curl --location --request POST '{{host}}/shareAuth/finish' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
"token": "sint dolore", "token": "{{authToken}}",
"responseData": [ "responseData": [
{ {
"moduleName": "KB Search", "moduleName": "KB Search",
@ -156,18 +237,18 @@ curl --location --request POST '{{host}}/shareAuth/finish' \
}' }'
``` ```
响应值与 chat 接口相同,增加了一个 token。可以重点关注`responseData`里的值price 与实际价格的倍率为`100000`
**此接口无需响应值**
## 使用示 ## 实践案
我们以[Laf作为服务器为例](https://laf.dev/),展示这 3 个接口的使用方式。 我们以[Laf作为服务器为例](https://laf.dev/)简单展示这 3 个接口的使用方式。
### 1. 创建3个Laf接口 ### 1. 创建3个Laf接口
![](/imgs/share-auth1.jpg) ![](/imgs/share-auth1.jpg)
{{< tabs tabTotal="3" >}} {{< tabs tabTotal="3" >}}
{{< tab tabName="/shareAuth/init" >}} {{< tab tabName="/shareAuth/init" >}}
{{< markdownify >}} {{< markdownify >}}
@ -179,13 +260,15 @@ import cloud from '@lafjs/cloud'
export default async function (ctx: FunctionContext) { export default async function (ctx: FunctionContext) {
const { token } = ctx.body const { token } = ctx.body
// 此处省略 token 解码过程
if (token === 'fastgpt') { if (token === 'fastgpt') {
return { success: true } return { success: true, data: { uid: "user1" } }
} }
return { success: false,message: "身份错误" } return { success: false,message:"身份错误" }
} }
``` ```
{{< /markdownify >}} {{< /markdownify >}}
@ -201,8 +284,8 @@ import cloud from '@lafjs/cloud'
export default async function (ctx: FunctionContext) { export default async function (ctx: FunctionContext) {
const { token, question } = ctx.body const { token, question } = ctx.body
console.log(token, question, 'start')
// 此处省略 token 解码过程
if (token !== 'fastgpt') { if (token !== 'fastgpt') {
return { success: false, message: "身份错误" } return { success: false, message: "身份错误" }
@ -212,8 +295,9 @@ export default async function (ctx: FunctionContext) {
return { success: false, message: "内容不合规" } return { success: false, message: "内容不合规" }
} }
return { success: true } return { success: true, data: { uid: "user1" } }
} }
``` ```
{{< /markdownify >}} {{< /markdownify >}}
@ -229,7 +313,12 @@ import cloud from '@lafjs/cloud'
export default async function (ctx: FunctionContext) { export default async function (ctx: FunctionContext) {
const { token, responseData } = ctx.body const { token, responseData } = ctx.body
console.log(token,responseData,'=====')
const total = responseData.reduce((sum,item) => sum + item.price,0)
const amount = total / 100000
// 省略数据库操作
return { } return { }
} }
``` ```
@ -241,17 +330,24 @@ export default async function (ctx: FunctionContext) {
### 2. 配置校验地址 ### 2. 配置校验地址
我们随便复制3个地址中一个接口https://d8dns0.laf.dev/shareAuth/finish , 去除 /shareAuth/finish 后填入 FastGPT 中: https://d8dns0.laf.dev 我们随便复制3个地址中一个接口: `https://d8dns0.laf.dev/shareAuth/finish`, 去除`/shareAuth/finish`后填入`身份校验`:`https://d8dns0.laf.dev`
![](/imgs/share-auth2.jpg) ![](/imgs/share-auth2.jpg)
### 3. 修改分享链接参数 ### 3. 修改分享链接参数
源分享链接:[https://fastgpt.run/chat/share?shareId=64be36376a438af0311e599c](https://fastgpt.run/chat/share?shareId=64be36376a438af0311e599c) 源分享链接:`https://fastgpt.run/chat/share?shareId=64be36376a438af0311e599c`
修改后:[https://fastgpt.run/chat/share?shareId=64be36376a438af0311e599c&authToken=fastgpt](https://fastgpt.run/chat/share?shareId=64be36376a438af0311e599c&authToken=fastgpt) 修改后:`https://fastgpt.run/chat/share?shareId=64be36376a438af0311e599c&authToken=fastgpt`
### 4. 测试效果 ### 4. 测试效果
1. 打开源链接或者`authToken`不等于 `fastgpt`的链接会提示身份错误。 1. 打开源链接或者`authToken`不等于`fastgpt`的链接会提示身份错误。
2. 发送内容中包含你字,会提示内容不合规。 2. 发送内容中包含你字,会提示内容不合规。
## 使用场景
这个鉴权方式通常是帮助你直接嵌入`分享链接`到你的应用中,在你的应用打开分享链接前,应做`authToken`的拼接后再打开。
除了对接已有系统的用户外,你还可以对接`余额`功能,通过`结果上报`接口扣除用户余额,通过`对话前校验`接口检查用户的余额。

View File

@ -4,7 +4,9 @@ import { ErrType } from '../errorCode';
export enum OutLinkErrEnum { export enum OutLinkErrEnum {
unExist = 'unExist', unExist = 'unExist',
unAuthLink = 'unAuthLink', unAuthLink = 'unAuthLink',
linkUnInvalid = 'linkUnInvalid' linkUnInvalid = 'linkUnInvalid',
unAuthUser = 'unAuthUser'
} }
const errList = [ const errList = [
{ {
@ -19,6 +21,10 @@ const errList = [
code: 501, code: 501,
statusText: OutLinkErrEnum.linkUnInvalid, statusText: OutLinkErrEnum.linkUnInvalid,
message: '分享链接无效' message: '分享链接无效'
},
{
statusText: OutLinkErrEnum.unAuthUser,
message: '身份校验失败'
} }
]; ];
export default errList.reduce((acc, cur, index) => { export default errList.reduce((acc, cur, index) => {

View File

@ -1,33 +0,0 @@
import { ModuleItemType } from '../module/type';
import { AdminFbkType, ChatItemType, moduleDispatchResType } from './type';
export type UpdateHistoryProps = {
chatId: string;
customTitle?: string;
top?: boolean;
};
export type AdminUpdateFeedbackParams = AdminFbkType & {
chatItemId: string;
};
export type InitChatResponse = {
chatId: string;
appId: string;
app: {
userGuideModule?: ModuleItemType;
chatModels?: string[];
name: string;
avatar: string;
intro: string;
canUse?: boolean;
};
title: string;
variables: Record<string, any>;
history: ChatItemType[];
};
export type ChatHistoryItemResType = moduleDispatchResType & {
moduleType: `${FlowNodeTypeEnum}`;
moduleName: string;
};

View File

@ -44,6 +44,12 @@ export const ChatSourceMap = {
} }
}; };
export enum ChatStatusEnum {
loading = 'loading',
running = 'running',
finish = 'finish'
}
export const HUMAN_ICON = `/icon/human.svg`; export const HUMAN_ICON = `/icon/human.svg`;
export const LOGO_ICON = `/icon/logo.svg`; export const LOGO_ICON = `/icon/logo.svg`;

View File

@ -1,6 +1,6 @@
import { ClassifyQuestionAgentItemType } from '../module/type'; import { ClassifyQuestionAgentItemType } from '../module/type';
import { SearchDataResponseItemType } from '../dataset/type'; import { SearchDataResponseItemType } from '../dataset/type';
import { ChatRoleEnum, ChatSourceEnum } from './constants'; import { ChatRoleEnum, ChatSourceEnum, ChatStatusEnum } from './constants';
import { FlowNodeTypeEnum } from '../module/node/constant'; import { FlowNodeTypeEnum } from '../module/node/constant';
import { ModuleOutputKeyEnum } from '../module/constants'; import { ModuleOutputKeyEnum } from '../module/constants';
import { AppSchema } from '../app/type'; import { AppSchema } from '../app/type';
@ -20,7 +20,7 @@ export type ChatSchema = {
variables: Record<string, any>; variables: Record<string, any>;
source: `${ChatSourceEnum}`; source: `${ChatSourceEnum}`;
shareId?: string; shareId?: string;
isInit: boolean; outLinkUid?: string;
content: ChatItemType[]; content: ChatItemType[];
}; };
@ -51,6 +51,7 @@ export type AdminFbkType = {
a?: string; a?: string;
}; };
/* --------- chat item ---------- */
export type ChatItemType = { export type ChatItemType = {
dataId?: string; dataId?: string;
obj: ChatItemSchema['obj']; obj: ChatItemSchema['obj'];
@ -61,11 +62,12 @@ export type ChatItemType = {
}; };
export type ChatSiteItemType = ChatItemType & { export type ChatSiteItemType = ChatItemType & {
status: 'loading' | 'running' | 'finish'; status: `${ChatStatusEnum}`;
moduleName?: string; moduleName?: string;
ttsBuffer?: Uint8Array; ttsBuffer?: Uint8Array;
}; };
/* ---------- history ------------- */
export type HistoryItemType = { export type HistoryItemType = {
chatId: string; chatId: string;
updateTime: Date; updateTime: Date;
@ -77,10 +79,10 @@ export type ChatHistoryItemType = HistoryItemType & {
top: boolean; top: boolean;
}; };
// response data /* ------- response data ------------ */
export type moduleDispatchResType = { export type moduleDispatchResType = {
moduleLogo?: string; moduleLogo?: string;
price: number; price?: number;
runningTime?: number; runningTime?: number;
tokens?: number; tokens?: number;
model?: string; model?: string;
@ -112,3 +114,8 @@ export type moduleDispatchResType = {
// plugin output // plugin output
pluginOutput?: Record<string, any>; pluginOutput?: Record<string, any>;
}; };
export type ChatHistoryItemResType = moduleDispatchResType & {
moduleType: `${FlowNodeTypeEnum}`;
moduleName: string;
};

View File

@ -6,9 +6,9 @@ import type { LLMModelItemType } from '../ai/model.d';
export type DatasetUpdateBody = { export type DatasetUpdateBody = {
id: string; id: string;
parentId?: string; parentId?: string;
tags?: string[];
name?: string; name?: string;
avatar?: string; avatar?: string;
intro?: string;
permission?: DatasetSchemaType['permission']; permission?: DatasetSchemaType['permission'];
agentModel?: LLMModelItemType; agentModel?: LLMModelItemType;
websiteConfig?: DatasetSchemaType['websiteConfig']; websiteConfig?: DatasetSchemaType['websiteConfig'];

View File

@ -1,27 +1,12 @@
import type { HistoryItemType, ChatSiteItemType } from '../../core/chat/type.d'; import type { HistoryItemType, ChatSiteItemType } from '../../core/chat/type.d';
import type { InitChatResponse } from '../../core/chat/api.d';
import { OutLinkSchema } from '@fastgpt/global/support/outLink/type'; import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
export type InitShareChatResponse = { export type AuthOutLinkInitProps = {
userAvatar: string; outLinkUid: string;
app: InitChatResponse['app'];
};
/* one page type */
export type ShareChatType = InitShareChatResponse & {
history: ShareChatHistoryItemType;
};
/* history list item type */
export type ShareChatHistoryItemType = HistoryItemType & {
shareId: string;
variables?: Record<string, any>;
chats: ChatSiteItemType[];
};
export type AuthLinkChatProps = { ip?: string | null; authToken?: string; question: string };
export type AuthLinkLimitProps = AuthLinkChatProps & { outLink: OutLinkSchema };
export type AuthShareChatInitProps = {
authToken?: string;
tokenUrl?: string; tokenUrl?: string;
}; };
export type AuthOutLinkChatProps = { ip?: string | null; outLinkUid: string; question: string };
export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSchema };
export type AuthOutLinkResponse = {
uid: string;
};

View File

@ -3,7 +3,6 @@ import { OutLinkTypeEnum } from './constant';
export type OutLinkSchema = { export type OutLinkSchema = {
_id: string; _id: string;
shareId: string; shareId: string;
userId: string;
teamId: string; teamId: string;
tmbId: string; tmbId: string;
appId: string; appId: string;

View File

@ -45,4 +45,4 @@ export const TeamMemberStatusMap = {
color: 'red.600' color: 'red.600'
} }
}; };
export const leaveStatus = { $ne: TeamMemberStatusEnum.leave }; export const notLeaveStatus = { $ne: TeamMemberStatusEnum.leave };

View File

@ -1,5 +1,5 @@
import { UserModelSchema } from '../type'; import type { UserModelSchema } from '../type';
import { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant'; import type { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant';
export type TeamSchema = { export type TeamSchema = {
_id: string; _id: string;
@ -22,6 +22,16 @@ export type TeamMemberSchema = {
defaultTeam: boolean; defaultTeam: boolean;
}; };
export type TeamMemberWithUserSchema = TeamMemberSchema & {
userId: UserModelSchema;
};
export type TeamMemberWithTeamSchema = TeamMemberSchema & {
teamId: TeamSchema;
};
export type TeamMemberWithTeamAndUserSchema = TeamMemberWithTeamSchema & {
userId: UserModelSchema;
};
export type TeamItemType = { export type TeamItemType = {
userId: string; userId: string;
teamId: string; teamId: string;

View File

@ -26,9 +26,13 @@ export async function connectMongo({
bufferCommands: true, bufferCommands: true,
maxConnecting: Number(process.env.DB_MAX_LINK || 5), maxConnecting: Number(process.env.DB_MAX_LINK || 5),
maxPoolSize: Number(process.env.DB_MAX_LINK || 5), maxPoolSize: Number(process.env.DB_MAX_LINK || 5),
minPoolSize: Number(process.env.DB_MAX_LINK || 10) * 0.5, minPoolSize: Math.min(10, Number(process.env.DB_MAX_LINK || 10)),
connectTimeoutMS: 60000, connectTimeoutMS: 60000,
waitQueueTimeoutMS: 60000 waitQueueTimeoutMS: 60000,
socketTimeoutMS: 60000,
maxIdleTimeMS: 300000,
retryWrites: true,
retryReads: true
}); });
console.log('mongo connected'); console.log('mongo connected');

View File

@ -50,10 +50,6 @@ const ChatSchema = new Schema({
top: { top: {
type: Boolean type: Boolean
}, },
variables: {
type: Object,
default: {}
},
source: { source: {
type: String, type: String,
enum: Object.keys(ChatSourceMap), enum: Object.keys(ChatSourceMap),
@ -62,9 +58,17 @@ const ChatSchema = new Schema({
shareId: { shareId: {
type: String type: String
}, },
isInit: { outLinkUid: {
type: Boolean, type: String
default: false },
variables: {
type: Object,
default: {}
},
metadata: {
//For special storage
type: Object,
default: {}
}, },
content: { content: {
type: [ type: [
@ -89,9 +93,10 @@ const ChatSchema = new Schema({
}); });
try { try {
ChatSchema.index({ tmbId: 1 });
ChatSchema.index({ updateTime: -1 });
ChatSchema.index({ appId: 1 }); ChatSchema.index({ appId: 1 });
ChatSchema.index({ tmbId: 1 });
ChatSchema.index({ shareId: 1 });
ChatSchema.index({ updateTime: -1 });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -0,0 +1,22 @@
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { MongoChatItem } from './chatItemSchema';
export async function getChatItems({
chatId,
limit = 30,
field
}: {
chatId?: string;
limit?: number;
field: string;
}): Promise<{ history: ChatItemType[] }> {
if (!chatId) {
return { history: [] };
}
const history = await MongoChatItem.find({ chatId }, field).sort({ _id: -1 }).limit(limit);
history.reverse();
return { history };
}

View File

@ -12,10 +12,6 @@ const OutLinkSchema = new Schema({
type: String, type: String,
required: true required: true
}, },
userId: {
type: Schema.Types.ObjectId,
ref: 'user'
},
teamId: { teamId: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: TeamCollectionName, ref: TeamCollectionName,

View File

@ -22,15 +22,15 @@ export const updateOutLinkUsage = async ({
}; };
export const pushResult2Remote = async ({ export const pushResult2Remote = async ({
authToken, outLinkUid,
shareId, shareId,
responseData responseData
}: { }: {
authToken?: string; outLinkUid?: string; // raw id, not parse
shareId?: string; shareId?: string;
responseData?: any[]; responseData?: any[];
}) => { }) => {
if (!shareId || !authToken || !global.systemEnv.pluginBaseUrl) return; if (!shareId || !outLinkUid || !global.systemEnv.pluginBaseUrl) return;
try { try {
const outLink = await MongoOutLink.findOne({ const outLink = await MongoOutLink.findOne({
shareId shareId
@ -42,7 +42,7 @@ export const pushResult2Remote = async ({
baseURL: outLink.limit.hookUrl, baseURL: outLink.limit.hookUrl,
url: '/shareAuth/finish', url: '/shareAuth/finish',
data: { data: {
token: authToken, token: outLinkUid,
responseData responseData
} }
}); });

View File

@ -19,6 +19,7 @@ export async function authApp({
AuthResponseType & { AuthResponseType & {
teamOwner: boolean; teamOwner: boolean;
app: AppDetailType; app: AppDetailType;
role: `${TeamMemberRoleEnum}`;
} }
> { > {
const result = await parseHeaderCert(props); const result = await parseHeaderCert(props);
@ -65,6 +66,7 @@ export async function authApp({
return { return {
...result, ...result,
app, app,
role,
isOwner, isOwner,
canWrite, canWrite,
teamOwner: role === TeamMemberRoleEnum.owner teamOwner: role === TeamMemberRoleEnum.owner

View File

@ -1,67 +0,0 @@
import { AuthResponseType } from '@fastgpt/global/support/permission/type';
import { AuthModeType } from '../type';
import type { ChatWithAppSchema } from '@fastgpt/global/core/chat/type';
import { parseHeaderCert } from '../controller';
import { MongoChat } from '../../../core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getTeamInfoByTmbId } from '../../user/team/controller';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant';
export async function authChat({
chatId,
per = 'owner',
...props
}: AuthModeType & {
chatId: string;
}): Promise<
AuthResponseType & {
chat: ChatWithAppSchema;
}
> {
const { userId, teamId, tmbId } = await parseHeaderCert(props);
const { role } = await getTeamInfoByTmbId({ tmbId });
const { chat, isOwner, canWrite } = await (async () => {
// get chat
const chat = (await MongoChat.findOne({ chatId, teamId })
.populate('appId')
.lean()) as ChatWithAppSchema;
if (!chat) {
return Promise.reject('Chat is not exists');
}
const isOwner = role === TeamMemberRoleEnum.owner || String(chat.tmbId) === tmbId;
const canWrite = isOwner;
if (
per === 'r' &&
role !== TeamMemberRoleEnum.owner &&
chat.appId.permission !== PermissionTypeEnum.public
) {
return Promise.reject(ChatErrEnum.unAuthChat);
}
if (per === 'w' && !canWrite) {
return Promise.reject(ChatErrEnum.unAuthChat);
}
if (per === 'owner' && !isOwner) {
return Promise.reject(ChatErrEnum.unAuthChat);
}
return {
chat,
isOwner,
canWrite
};
})();
return {
userId,
teamId,
tmbId,
chat,
isOwner,
canWrite
};
}

View File

@ -20,11 +20,11 @@ export async function authCertAndShareId({
return authCert(props); return authCert(props);
} }
const { app } = await authOutLinkValid({ shareId }); const { shareChat } = await authOutLinkValid({ shareId });
return { return {
teamId: String(app.teamId), teamId: String(shareChat.teamId),
tmbId: String(app.tmbId), tmbId: String(shareChat.tmbId),
authType: AuthUserTypeEnum.outLink, authType: AuthUserTypeEnum.outLink,
apikey: '', apikey: '',
isOwner: false, isOwner: false,

View File

@ -78,21 +78,19 @@ export async function authOutLinkCrud({
}; };
} }
/* outLink exist and it app exist */
export async function authOutLinkValid({ shareId }: { shareId?: string }) { export async function authOutLinkValid({ shareId }: { shareId?: string }) {
if (!shareId) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
const shareChat = await MongoOutLink.findOne({ shareId }); const shareChat = await MongoOutLink.findOne({ shareId });
if (!shareChat) { if (!shareChat) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid); return Promise.reject(OutLinkErrEnum.linkUnInvalid);
} }
const app = await MongoApp.findById(shareChat.appId);
if (!app) {
return Promise.reject(AppErrEnum.unExist);
}
return { return {
app, appId: shareChat.appId,
shareChat shareChat
}; };
} }

View File

@ -1,4 +1,8 @@
import { UserType } from '@fastgpt/global/support/user/type';
import { MongoUser } from './schema'; import { MongoUser } from './schema';
import { getTeamInfoByTmbId, getUserDefaultTeam } from './team/controller';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
export async function authUserExist({ userId, username }: { userId?: string; username?: string }) { export async function authUserExist({ userId, username }: { userId?: string; username?: string }) {
if (userId) { if (userId) {
@ -9,3 +13,56 @@ export async function authUserExist({ userId, username }: { userId?: string; use
} }
return null; return null;
} }
export async function getUserDetail({
tmbId,
userId
}: {
tmbId?: string;
userId?: string;
}): Promise<UserType> {
const team = await (async () => {
if (tmbId) {
return getTeamInfoByTmbId({ tmbId });
}
if (userId) {
return getUserDefaultTeam({ userId });
}
return Promise.reject(ERROR_ENUM.unAuthorization);
})();
const user = await MongoUser.findById(team.userId);
if (!user) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return {
_id: user._id,
username: user.username,
avatar: user.avatar,
balance: user.balance,
timezone: user.timezone,
promotionRate: user.promotionRate,
openaiAccount: user.openaiAccount,
team
};
}
export async function getUserAndAuthBalance({
tmbId,
minBalance
}: {
tmbId: string;
minBalance?: number;
}) {
const user = await getUserDetail({ tmbId });
if (!user) {
return Promise.reject(UserErrEnum.unAuthUser);
}
if (minBalance !== undefined && user.team.balance < minBalance) {
return Promise.reject(UserErrEnum.balanceNotEnough);
}
return user;
}

View File

@ -1,35 +1,15 @@
import { TeamItemType } from '@fastgpt/global/support/user/team/type'; import { TeamItemType, TeamMemberWithTeamSchema } from '@fastgpt/global/support/user/team/type';
import { connectionMongo, Types } from '../../../common/mongo'; import { Types } from '../../../common/mongo';
import { import {
TeamMemberRoleEnum, TeamMemberRoleEnum,
TeamMemberStatusEnum, TeamMemberStatusEnum,
TeamCollectionName, notLeaveStatus
TeamMemberCollectionName,
leaveStatus
} from '@fastgpt/global/support/user/team/constant'; } from '@fastgpt/global/support/user/team/constant';
import { MongoTeamMember } from './teamMemberSchema';
import { MongoTeam } from './teamSchema';
async function getTeam(match: Record<string, any>): Promise<TeamItemType> { async function getTeam(match: Record<string, any>): Promise<TeamItemType> {
const db = connectionMongo?.connection?.db; const tmb = (await MongoTeamMember.findOne(match).populate('teamId')) as TeamMemberWithTeamSchema;
const TeamMember = db.collection(TeamMemberCollectionName);
const results = await TeamMember.aggregate([
{
$match: match
},
{
$lookup: {
from: TeamCollectionName,
localField: 'teamId',
foreignField: '_id',
as: 'team'
}
},
{
$unwind: '$team'
}
]).toArray();
const tmb = results[0];
if (!tmb) { if (!tmb) {
return Promise.reject('member not exist'); return Promise.reject('member not exist');
@ -37,17 +17,17 @@ async function getTeam(match: Record<string, any>): Promise<TeamItemType> {
return { return {
userId: String(tmb.userId), userId: String(tmb.userId),
teamId: String(tmb.teamId), teamId: String(tmb.teamId._id),
teamName: tmb.team.name, teamName: tmb.teamId.name,
memberName: tmb.name, memberName: tmb.name,
avatar: tmb.team.avatar, avatar: tmb.teamId.avatar,
balance: tmb.team.balance, balance: tmb.teamId.balance,
tmbId: String(tmb._id), tmbId: String(tmb._id),
role: tmb.role, role: tmb.role,
status: tmb.status, status: tmb.status,
defaultTeam: tmb.defaultTeam, defaultTeam: tmb.defaultTeam,
canWrite: tmb.role !== TeamMemberRoleEnum.visitor, canWrite: tmb.role !== TeamMemberRoleEnum.visitor,
maxSize: tmb.team.maxSize maxSize: tmb.teamId.maxSize
}; };
} }
@ -57,7 +37,7 @@ export async function getTeamInfoByTmbId({ tmbId }: { tmbId: string }) {
} }
return getTeam({ return getTeam({
_id: new Types.ObjectId(tmbId), _id: new Types.ObjectId(tmbId),
status: leaveStatus status: notLeaveStatus
}); });
} }
@ -83,12 +63,8 @@ export async function createDefaultTeam({
balance?: number; balance?: number;
maxSize?: number; maxSize?: number;
}) { }) {
const db = connectionMongo.connection.db;
const Team = db.collection(TeamCollectionName);
const TeamMember = db.collection(TeamMemberCollectionName);
// auth default team // auth default team
const tmb = await TeamMember.findOne({ const tmb = await MongoTeamMember.findOne({
userId: new Types.ObjectId(userId), userId: new Types.ObjectId(userId),
defaultTeam: true defaultTeam: true
}); });
@ -97,7 +73,7 @@ export async function createDefaultTeam({
console.log('create default team', userId); console.log('create default team', userId);
// create // create
const { insertedId } = await Team.insertOne({ const { _id: insertedId } = await MongoTeam.create({
ownerId: userId, ownerId: userId,
name: teamName, name: teamName,
avatar, avatar,
@ -105,7 +81,7 @@ export async function createDefaultTeam({
maxSize, maxSize,
createTime: new Date() createTime: new Date()
}); });
await TeamMember.insertOne({ await MongoTeamMember.create({
teamId: insertedId, teamId: insertedId,
userId, userId,
name: 'Owner', name: 'Owner',
@ -116,16 +92,11 @@ export async function createDefaultTeam({
}); });
} else { } else {
console.log('default team exist', userId); console.log('default team exist', userId);
await Team.updateOne( await MongoTeam.findByIdAndUpdate(tmb.teamId, {
{ $set: {
_id: new Types.ObjectId(tmb.teamId) ...(balance !== undefined && { balance }),
}, maxSize
{
$set: {
...(balance !== undefined && { balance }),
maxSize
}
} }
); });
} }
} }

View File

@ -0,0 +1,51 @@
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { TeamMemberSchema as TeamMemberType } from '@fastgpt/global/support/user/team/type.d';
import { userCollectionName } from '../../user/schema';
import {
TeamMemberRoleMap,
TeamMemberStatusMap,
TeamMemberCollectionName,
TeamCollectionName
} from '@fastgpt/global/support/user/team/constant';
const TeamMemberSchema = new Schema({
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: userCollectionName,
required: true
},
name: {
type: String,
default: 'Member'
},
role: {
type: String,
enum: Object.keys(TeamMemberRoleMap)
},
status: {
type: String,
enum: Object.keys(TeamMemberStatusMap)
},
createTime: {
type: Date,
default: () => new Date()
},
defaultTeam: {
type: Boolean,
default: false
}
});
try {
} catch (error) {
console.log(error);
}
export const MongoTeamMember: Model<TeamMemberType> =
models[TeamMemberCollectionName] || model(TeamMemberCollectionName, TeamMemberSchema);

View File

@ -0,0 +1,41 @@
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { TeamSchema as TeamType } from '@fastgpt/global/support/user/team/type.d';
import { userCollectionName } from '../../user/schema';
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { PRICE_SCALE } from '@fastgpt/global/support/wallet/bill/constants';
const TeamSchema = new Schema({
name: {
type: String,
required: true
},
ownerId: {
type: Schema.Types.ObjectId,
ref: userCollectionName
},
avatar: {
type: String,
default: '/icon/logo.svg'
},
createTime: {
type: Date,
default: () => Date.now()
},
balance: {
type: Number,
default: 2 * PRICE_SCALE
},
maxSize: {
type: Number,
default: 5
}
});
try {
} catch (error) {
console.log(error);
}
export const MongoTeam: Model<TeamType> =
models[TeamCollectionName] || model(TeamCollectionName, TeamSchema);

View File

@ -327,12 +327,12 @@
"QA Prompt": "QA Prompt", "QA Prompt": "QA Prompt",
"Start Sync Tip": "Are you sure to start synchronizing data? The old data will be deleted and then re-acquired, please confirm!", "Start Sync Tip": "Are you sure to start synchronizing data? The old data will be deleted and then re-acquired, please confirm!",
"Sync": "Data Sync", "Sync": "Data Sync",
"Sync Collection": "Data Sync",
"Website Create Success": "Created successfully, data is being synchronized", "Website Create Success": "Created successfully, data is being synchronized",
"Website Empty Tip": "No associated website yet", "Website Empty Tip": "No associated website yet",
"Website Link": "Website Link", "Website Link": "Website Link",
"Website Sync": "Website", "Website Sync": "Website",
"id": "Id", "id": "Id",
"Sync Collection": "Data Sync",
"metadata": { "metadata": {
"Chunk Size": "Chunk Size", "Chunk Size": "Chunk Size",
"Createtime": "Create Time", "Createtime": "Create Time",
@ -510,6 +510,10 @@
"variable options": "Options" "variable options": "Options"
}, },
"variable add option": "Add Option" "variable add option": "Add Option"
},
"shareChat": {
"Init Error": "Init Chat Error",
"Init History Error": "Init History Error"
} }
}, },
"dataset": { "dataset": {

View File

@ -316,7 +316,7 @@
"Search Top K": "单次搜索数量", "Search Top K": "单次搜索数量",
"Set Empty Result Tip": ",未搜索到内容时回复指定内容", "Set Empty Result Tip": ",未搜索到内容时回复指定内容",
"Set Website Config": "开始配置网站信息", "Set Website Config": "开始配置网站信息",
"Similarity": "相度", "Similarity": "相度",
"Sync Time": "最后更新时间", "Sync Time": "最后更新时间",
"Virtual File": "虚拟文件", "Virtual File": "虚拟文件",
"Website Dataset": "Web 站点同步", "Website Dataset": "Web 站点同步",
@ -327,12 +327,12 @@
"QA Prompt": "QA 拆分引导词", "QA Prompt": "QA 拆分引导词",
"Start Sync Tip": "确认开始同步数据?将会删除旧数据后重新获取,请确认!", "Start Sync Tip": "确认开始同步数据?将会删除旧数据后重新获取,请确认!",
"Sync": "同步数据", "Sync": "同步数据",
"Sync Collection": "数据同步",
"Website Create Success": "创建成功,正在同步数据", "Website Create Success": "创建成功,正在同步数据",
"Website Empty Tip": "还没有关联网站", "Website Empty Tip": "还没有关联网站",
"Website Link": "Web 站点地址", "Website Link": "Web 站点地址",
"Website Sync": "Web 站点同步", "Website Sync": "Web 站点同步",
"id": "集合ID", "id": "集合ID",
"Sync Collection": "数据同步",
"metadata": { "metadata": {
"Chunk Size": "分割大小", "Chunk Size": "分割大小",
"Createtime": "创建时间", "Createtime": "创建时间",
@ -405,17 +405,17 @@
"search": { "search": {
"Empty result response": "空搜索回复", "Empty result response": "空搜索回复",
"Empty result response Tips": "若填写该内容,没有搜索到合适内容时,将直接回复填写的内容。", "Empty result response Tips": "若填写该内容,没有搜索到合适内容时,将直接回复填写的内容。",
"Min Similarity": "最低相度", "Min Similarity": "最低相度",
"Min Similarity Tips": "不同索引模型的相度有区别,请通过搜索测试来选择合适的数值", "Min Similarity Tips": "不同索引模型的相度有区别,请通过搜索测试来选择合适的数值,使用 ReRank 时,相关度可能会很低。",
"Params Setting": "搜索参数设置", "Params Setting": "搜索参数设置",
"Top K": "单次搜索上限", "Top K": "单次搜索上限",
"mode": { "mode": {
"embFullTextReRank": "混合检索", "embFullTextReRank": "混合检索",
"embFullTextReRank desc": "使用向量检索与全文检索混合结果进行 Rerank 进行重排,通常效果最佳", "embFullTextReRank desc": "使用向量检索与全文检索混合结果进行 Rerank 进行重排,相关度通常差异明显,推荐。",
"embedding": "语义检索", "embedding": "语义检索",
"embedding desc": "直接进行向量 topk 相关性查询", "embedding desc": "直接进行向量 topk 相关性查询",
"embeddingReRank": "增强语义检索", "embeddingReRank": "增强语义检索",
"embeddingReRank desc": "超额进行向量 topk 查询后再使用 Rerank 进行排序" "embeddingReRank desc": "超额进行向量 topk 查询后再使用 Rerank 进行排序,相关度通常差异明显。"
}, },
"search mode": "检索模式" "search mode": "检索模式"
}, },
@ -510,6 +510,10 @@
"variable options": "选项" "variable options": "选项"
}, },
"variable add option": "添加选项" "variable add option": "添加选项"
},
"shareChat": {
"Init Error": "初始化对话框失败",
"Init History Error": "初始化聊天记录失败"
} }
}, },
"dataset": { "dataset": {

View File

@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api.d'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { Flex, BoxProps, useDisclosure, Image, useTheme, Box } from '@chakra-ui/react'; import { Flex, BoxProps, useDisclosure, Image, useTheme, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';

View File

@ -1,6 +1,6 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Box, useTheme, Flex, Image } from '@chakra-ui/react'; import { Box, useTheme, Flex, Image } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api.d'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { moduleTemplatesFlat } from '@/web/core/modules/template/system'; import { moduleTemplatesFlat } from '@/web/core/modules/template/system';
import Tabs from '../Tabs'; import Tabs from '../Tabs';

View File

@ -12,7 +12,7 @@ import Script from 'next/script';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import type { ExportChatType } from '@/types/chat.d'; import type { ExportChatType } from '@/types/chat.d';
import type { ChatItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type.d'; import type { ChatItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type.d';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api.d'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useToast } from '@/web/common/hooks/useToast'; import { useToast } from '@/web/common/hooks/useToast';
import { useAudioPlay } from '@/web/common/utils/voice'; import { useAudioPlay } from '@/web/common/utils/voice';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
@ -80,7 +80,7 @@ export type StartChatFnProps = {
}; };
export type ComponentRef = { export type ComponentRef = {
getChatHistory: () => ChatSiteItemType[]; getChatHistories: () => ChatSiteItemType[];
resetVariables: (data?: Record<string, any>) => void; resetVariables: (data?: Record<string, any>) => void;
resetHistory: (history: ChatSiteItemType[]) => void; resetHistory: (history: ChatSiteItemType[]) => void;
scrollToBottom: (behavior?: 'smooth' | 'auto') => void; scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
@ -134,7 +134,7 @@ const ChatBox = (
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const { isPc } = useSystemStore(); const { isPc, setLoading } = useSystemStore();
const TextareaDom = useRef<HTMLTextAreaElement>(null); const TextareaDom = useRef<HTMLTextAreaElement>(null);
const chatController = useRef(new AbortController()); const chatController = useRef(new AbortController());
const questionGuideController = useRef(new AbortController()); const questionGuideController = useRef(new AbortController());
@ -415,15 +415,20 @@ const ChatBox = (
async (index: number) => { async (index: number) => {
if (!onDelMessage) return; if (!onDelMessage) return;
const delHistory = chatHistory.slice(index); const delHistory = chatHistory.slice(index);
setChatHistory((state) => (index === 0 ? [] : state.slice(0, index)));
await Promise.all( setLoading(true);
delHistory.map((item, i) => onDelMessage({ contentId: item.dataId, index: index + i }))
);
sendPrompt(variables, delHistory[0].value, chatHistory.slice(0, index)); try {
await Promise.all(
delHistory.map((item, i) => onDelMessage({ contentId: item.dataId, index: index + i }))
);
setChatHistory((state) => (index === 0 ? [] : state.slice(0, index)));
sendPrompt(variables, delHistory[0].value, chatHistory.slice(0, index));
} catch (error) {}
setLoading(false);
}, },
[chatHistory, onDelMessage, sendPrompt, variables] [chatHistory, onDelMessage, sendPrompt, setLoading, variables]
); );
// delete one message // delete one message
const delOneMessage = useCallback( const delOneMessage = useCallback(
@ -439,7 +444,7 @@ const ChatBox = (
// output data // output data
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getChatHistory: () => chatHistory, getChatHistories: () => chatHistory,
resetVariables(e) { resetVariables(e) {
const defaultVal: Record<string, any> = {}; const defaultVal: Record<string, any> = {};
variableModules?.forEach((item) => { variableModules?.forEach((item) => {

View File

@ -86,7 +86,7 @@ const DatasetParamsModal = ({
min={0} min={0}
max={1} max={1}
step={0.01} step={0.01}
value={getValues(ModuleInputKeyEnum.datasetSimilarity) || 0.5} value={getValues(ModuleInputKeyEnum.datasetSimilarity) ?? 0.5}
onChange={(val) => { onChange={(val) => {
setValue(ModuleInputKeyEnum.datasetSimilarity, val); setValue(ModuleInputKeyEnum.datasetSimilarity, val);
setRefresh(!refresh); setRefresh(!refresh);
@ -107,7 +107,7 @@ const DatasetParamsModal = ({
]} ]}
min={1} min={1}
max={30} max={30}
value={getValues(ModuleInputKeyEnum.datasetLimit) || 5} value={getValues(ModuleInputKeyEnum.datasetLimit) ?? 5}
onChange={(val) => { onChange={(val) => {
setValue(ModuleInputKeyEnum.datasetLimit, val); setValue(ModuleInputKeyEnum.datasetLimit, val);
setRefresh(!refresh); setRefresh(!refresh);

View File

@ -1,7 +1,75 @@
import type { AppTTSConfigType } from '@fastgpt/global/core/module/type.d'; import type { AppTTSConfigType } from '@fastgpt/global/core/module/type.d';
import { ModuleItemType } from '../module/type';
import { AdminFbkType, ChatItemType, moduleDispatchResType } from '@fastgpt/global/core/chat/type';
export type GetChatSpeechProps = { export type GetChatSpeechProps = {
ttsConfig: AppTTSConfigType; ttsConfig: AppTTSConfigType;
input: string; input: string;
shareId?: string; shareId?: string;
}; };
/* ---------- chat ----------- */
export type InitChatProps = {
appId?: string;
chatId?: string;
};
export type InitOutLinkChatProps = {
chatId?: string;
shareId?: string;
outLinkUid?: string;
};
export type InitChatResponse = {
chatId?: string;
appId: string;
userAvatar?: string;
title: string;
variables: Record<string, any>;
history: ChatItemType[];
app: {
userGuideModule?: ModuleItemType;
chatModels?: string[];
name: string;
avatar: string;
intro: string;
canUse?: boolean;
};
};
/* ---------- history ----------- */
export type getHistoriesProps = {
appId?: string;
// share chat
shareId?: string;
outLinkUid?: string; // authToken/uid
};
export type UpdateHistoryProps = {
chatId: string;
customTitle?: string;
top?: boolean;
shareId?: string;
outLinkUid?: string;
};
export type DelHistoryProps = {
chatId: string;
shareId?: string;
outLinkUid?: string;
};
export type ClearHistoriesProps = {
appId?: string;
shareId?: string;
outLinkUid?: string;
};
/* -------- chat item ---------- */
export type DeleteChatItemProps = {
chatId: string;
contentId: string;
shareId?: string;
outLinkUid?: string;
};
export type AdminUpdateFeedbackParams = AdminFbkType & {
chatItemId: string;
};

View File

@ -0,0 +1,15 @@
import { InitChatResponse } from './api';
export const defaultChatData: InitChatResponse = {
chatId: '',
appId: '',
app: {
name: 'Loading',
avatar: '/icon/logo.svg',
intro: '',
canUse: false
},
title: '新对话',
variables: {},
history: []
};

View File

@ -33,9 +33,8 @@ export const Prompt_QuotePromptList: PromptTemplateItem[] = [
""" """
1. 使 1. 使
2. 2.
3. 3. markdown markdown
4. markdown markdown
:"{{question}}"` :"{{question}}"`
}, },
{ {
@ -47,9 +46,8 @@ export const Prompt_QuotePromptList: PromptTemplateItem[] = [
""" """
1. 使 instruction output 1. 使 instruction output
2. 2.
3. 3. markdown markdown
4. markdown markdown
:"{{question}}"` :"{{question}}"`
}, },
{ {

View File

@ -54,7 +54,7 @@ const PayModal = ({ onClose }: { onClose: () => void }) => {
onSuccess(res) { onSuccess(res) {
if (!res) return; if (!res) return;
toast({ toast({
title: '充值成功', title: res,
status: 'success' status: 'success'
}); });
router.reload(); router.reload();

View File

@ -1,32 +1,14 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema';
import { startQueue } from '@/service/utils/tools'; import { startQueue } from '@/service/utils/tools';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
await connectToDatabase(); await connectToDatabase();
const { userId } = await authCert({ req, authToken: true }); await authCert({ req, authToken: true });
await unlockTask(userId); startQueue();
} catch (error) {} } catch (error) {}
jsonRes(res); jsonRes(res);
} }
async function unlockTask(userId: string) {
try {
await MongoDatasetTraining.updateMany(
{
userId
},
{
lockTime: new Date('2000/1/1')
}
);
startQueue();
} catch (error) {
unlockTask(userId);
}
}

View File

@ -5,6 +5,7 @@ import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@ -20,6 +21,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await authApp({ req, authToken: true, appId, per: 'owner' }); await authApp({ req, authToken: true, appId, per: 'owner' });
// 删除对应的聊天 // 删除对应的聊天
await MongoChatItem.deleteMany({
appId
});
await MongoChat.deleteMany({ await MongoChat.deleteMany({
appId appId
}); });

View File

@ -381,7 +381,7 @@ function datasetTemplate({
key: 'similarity', key: 'similarity',
value: 0.4, value: 0.4,
type: FlowNodeInputTypeEnum.slider, type: FlowNodeInputTypeEnum.slider,
label: '相度', label: '相度',
connected: true connected: true
}, },
{ {

View File

@ -289,7 +289,7 @@ function datasetTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
key: 'similarity', key: 'similarity',
value: formData.dataset.similarity, value: formData.dataset.similarity,
type: FlowNodeInputTypeEnum.slider, type: FlowNodeInputTypeEnum.slider,
label: '相度', label: '相度',
connected: false connected: false
}, },
{ {

View File

@ -8,8 +8,9 @@ import { pushChatBill } from '@/service/support/wallet/bill/push';
import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants'; import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authUser } from '@/service/support/permission/auth/user';
import { dispatchModules } from '@/service/moduleDispatch'; import { dispatchModules } from '@/service/moduleDispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserAndAuthBalance } from '@fastgpt/service/support/user/controller';
export type Props = { export type Props = {
history: ChatItemType[]; history: ChatItemType[];
@ -40,15 +41,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
/* user auth */ /* user auth */
const [{ teamId, tmbId }, { user }] = await Promise.all([ const [_, { teamId, tmbId }] = await Promise.all([
authApp({ req, authToken: true, appId, per: 'r' }), authApp({ req, authToken: true, appId, per: 'r' }),
authUser({ authCert({
req, req,
authToken: true, authToken: true
minBalance: 0
}) })
]); ]);
// auth balance
const user = await getUserAndAuthBalance({
tmbId,
minBalance: 0
});
/* start process */ /* start process */
const { responseData } = await dispatchModules({ const { responseData } = await dispatchModules({
res, res,

View File

@ -0,0 +1,58 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { ClearHistoriesProps } from '@/global/core/chat/api';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
/* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { appId, shareId, outLinkUid } = req.query as ClearHistoriesProps;
const match = await (async () => {
if (shareId && outLinkUid) {
const { uid } = await authOutLink({ shareId, outLinkUid });
return {
shareId,
outLinkUid: uid
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true });
return {
appId,
tmbId,
source: ChatSourceEnum.online
};
}
return Promise.reject('Param are error');
})();
console.log(match);
// find chatIds
const list = await MongoChat.find(match, 'chatId').lean();
const idList = list.map((item) => item.chatId);
await MongoChatItem.deleteMany({
chatId: { $in: idList }
});
await MongoChat.deleteMany({
chatId: { $in: idList }
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -0,0 +1,38 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { DelHistoryProps } from '@/global/core/chat/api';
import { autChatCrud } from '@/service/support/permission/auth/chat';
/* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { chatId, shareId, outLinkUid } = req.query as DelHistoryProps;
await autChatCrud({
req,
authToken: true,
chatId,
shareId,
outLinkUid,
per: 'w'
});
await MongoChatItem.deleteMany({
chatId
});
await MongoChat.findOneAndRemove({
chatId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -1,54 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
type Props = {
chatId?: string;
appId?: string;
};
/* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { chatId, appId } = req.query as Props;
const { tmbId } = await authCert({ req, authToken: true });
if (chatId) {
await MongoChatItem.deleteMany({
chatId,
tmbId
});
await MongoChat.findOneAndRemove({
chatId,
tmbId
});
}
if (appId) {
const chats = await MongoChat.find({
appId,
tmbId,
source: ChatSourceEnum.online
}).select('_id');
const chatIds = chats.map((chat) => chat._id);
await MongoChatItem.deleteMany({
chatId: { $in: chatIds }
});
await MongoChat.deleteMany({
_id: { $in: chatIds }
});
}
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type { AdminUpdateFeedbackParams } from '@fastgpt/global/core/chat/api.d'; import type { AdminUpdateFeedbackParams } from '@/global/core/chat/api.d';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
/* 初始化我的聊天框,需要身份验证 */ /* 初始化我的聊天框,需要身份验证 */

View File

@ -0,0 +1,62 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { getHistoriesProps } from '@/global/core/chat/api';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { appId, shareId, outLinkUid } = req.body as getHistoriesProps;
const limit = shareId && outLinkUid ? 20 : 30;
const match = await (async () => {
if (shareId && outLinkUid) {
const { uid } = await authOutLink({ shareId, outLinkUid });
return {
shareId,
outLinkUid: uid,
source: ChatSourceEnum.share,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
}
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true });
return {
appId,
tmbId,
source: ChatSourceEnum.online
};
}
return Promise.reject('Params are error');
})();
const data = await MongoChat.find(match, 'chatId title top customTitle appId updateTime')
.sort({ top: -1, updateTime: -1 })
.limit(limit);
jsonRes<ChatHistoryItemType[]>(res, {
data: data.map((item) => ({
chatId: item.chatId,
updateTime: item.updateTime,
appId: item.appId,
customTitle: item.customTitle,
title: item.title,
top: item.top
}))
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -1,24 +1,20 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import type { InitChatResponse } from '@fastgpt/global/core/chat/api.d';
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { getGuideModule } from '@fastgpt/global/core/module/utils'; import { getGuideModule } from '@fastgpt/global/core/module/utils';
import { getChatModelNameListByModules } from '@/service/core/app/module'; import { getChatModelNameListByModules } from '@/service/core/app/module';
import { authChat } from '@fastgpt/service/support/permission/auth/chat';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants'; import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
await connectToDatabase(); await connectToDatabase();
let { appId, chatId } = req.query as { let { appId, chatId } = req.query as InitChatProps;
appId: string;
chatId: '' | string;
};
if (!appId) { if (!appId) {
return jsonRes(res, { return jsonRes(res, {
@ -27,57 +23,44 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
} }
// 校验使用权限 // auth app permission
const [{ app }, autChatResult] = await Promise.all([ const [{ app, tmbId }, chat] = await Promise.all([
authApp({ authApp({
req, req,
authToken: true, authToken: true,
appId, appId,
per: 'r' per: 'r'
}), }),
chatId chatId ? MongoChat.findOne({ chatId }) : undefined
? authChat({
req,
authToken: true,
chatId,
per: 'r'
})
: undefined
]); ]);
// get app and history // auth chat permission
const { history = [] }: { history?: ChatItemType[] } = await (async () => { if (!app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
if (chatId) { throw new Error(ChatErrEnum.unAuthChat);
// auth chatId }
const history = await MongoChatItem.find(
{
chatId
},
`dataId obj value adminFeedback userFeedback ${ModuleOutputKeyEnum.responseData}`
)
.sort({ _id: -1 })
.limit(30);
history.reverse(); // get app and history
return { history }; const { history } = await getChatItems({
} chatId,
return {}; limit: 30,
})(); field: `dataId obj value adminFeedback userFeedback ${ModuleOutputKeyEnum.responseData}`
});
jsonRes<InitChatResponse>(res, { jsonRes<InitChatResponse>(res, {
data: { data: {
chatId, chatId,
appId, appId,
title: chat?.title || '新对话',
userAvatar: undefined,
variables: chat?.variables || {},
history,
app: { app: {
userGuideModule: getGuideModule(app.modules), userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules), chatModels: getChatModelNameListByModules(app.modules),
name: app.name, name: app.name,
avatar: app.avatar, avatar: app.avatar,
intro: app.intro intro: app.intro
}, }
title: autChatResult?.chat?.title || '新对话',
variables: autChatResult?.chat?.variables || {},
history
} }
}); });
} catch (err) { } catch (err) {

View File

@ -0,0 +1,42 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
/* clear chat history */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { outLinkUid, chatIds } = req.body as {
outLinkUid: string;
chatIds: string[];
};
if (!outLinkUid) {
throw new Error('shareId or outLinkUid is required');
}
const sliceIds = chatIds.slice(0, 50);
await MongoChat.updateMany(
{
chatId: { $in: sliceIds },
source: ChatSourceEnum.share,
outLinkUid: { $exists: false }
},
{
$set: {
outLinkUid
}
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -2,14 +2,22 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { authChat } from '@fastgpt/service/support/permission/auth/chat'; import { autChatCrud } from '@/service/support/permission/auth/chat';
import type { DeleteChatItemProps } from '@/global/core/chat/api.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
await connectToDatabase(); await connectToDatabase();
const { chatId, contentId } = req.query as { chatId: string; contentId: string }; const { chatId, contentId, shareId, outLinkUid } = req.query as DeleteChatItemProps;
await authChat({ req, authToken: true, chatId, per: 'w' }); await autChatCrud({
req,
authToken: true,
chatId,
shareId,
outLinkUid,
per: 'w'
});
await MongoChatItem.deleteOne({ await MongoChatItem.deleteOne({
dataId: contentId, dataId: contentId,

View File

@ -1,43 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
/* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { appId } = req.body as { appId: string };
const { tmbId } = await authApp({ req, authToken: true, appId, per: 'r' });
const data = await MongoChat.find(
{
appId,
tmbId,
source: ChatSourceEnum.online
},
'chatId title top customTitle appId updateTime'
)
.sort({ top: -1, updateTime: -1 })
.limit(20);
jsonRes<ChatHistoryItemType[]>(res, {
data: data.map((item) => ({
chatId: item.chatId,
updateTime: item.updateTime,
appId: item.appId,
customTitle: item.customTitle,
title: item.title,
top: item.top
}))
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -0,0 +1,85 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d';
import { getGuideModule } from '@fastgpt/global/core/module/utils';
import { getChatModelNameListByModules } from '@/service/core/app/module';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { selectShareResponse } from '@/utils/service/core/chat';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
let { chatId, shareId, outLinkUid } = req.query as InitOutLinkChatProps;
// auth link permission
const { shareChat, uid, appId } = await authOutLink({ shareId, outLinkUid });
// auth app permission
const [tmb, chat, app] = await Promise.all([
MongoTeamMember.findById(shareChat.tmbId, '_id userId').populate('userId', 'avatar').lean(),
MongoChat.findOne({ chatId, shareId }).lean(),
MongoApp.findById(appId).lean()
]);
if (!app) {
throw new Error(AppErrEnum.unExist);
}
// auth chat permission
if (chat && chat.outLinkUid !== uid) {
throw new Error(ChatErrEnum.unAuthChat);
}
const { history } = await getChatItems({
chatId,
limit: 30,
field: `dataId obj value userFeedback ${
shareChat.responseDetail ? `adminFeedback ${ModuleOutputKeyEnum.responseData}` : ''
} `
});
// pick share response field
history.forEach((item) => {
item.responseData = selectShareResponse({ responseData: item.responseData });
});
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId: app._id,
title: chat?.title || '新对话',
//@ts-ignore
userAvatar: tmb?.userId?.avatar,
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
name: app.name,
avatar: app.avatar,
intro: app.intro
}
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export const config = {
api: {
responseLimit: '10mb'
}
};

View File

@ -1,17 +1,24 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { UpdateHistoryProps } from '@fastgpt/global/core/chat/api.d'; import { UpdateHistoryProps } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { authChat } from '@fastgpt/service/support/permission/auth/chat'; import { autChatCrud } from '@/service/support/permission/auth/chat';
/* 更新聊天标题 */ /* update chat top, custom title */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
await connectToDatabase(); await connectToDatabase();
const { chatId, customTitle, top } = req.body as UpdateHistoryProps; const { chatId, shareId, outLinkUid, customTitle, top } = req.body as UpdateHistoryProps;
await authChat({ req, authToken: true, chatId }); await autChatCrud({
req,
authToken: true,
chatId,
shareId,
outLinkUid,
per: 'w'
});
await MongoChat.findOneAndUpdate( await MongoChat.findOneAndUpdate(
{ chatId }, { chatId },

View File

@ -8,7 +8,7 @@ import { authDataset } from '@fastgpt/service/support/permission/auth/dataset';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
await connectToDatabase(); await connectToDatabase();
const { id, parentId, name, avatar, tags, permission, agentModel, websiteConfig, status } = const { id, parentId, name, avatar, intro, permission, agentModel, websiteConfig, status } =
req.body as DatasetUpdateBody; req.body as DatasetUpdateBody;
if (!id) { if (!id) {
@ -26,11 +26,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
...(parentId !== undefined && { parentId: parentId || null }), ...(parentId !== undefined && { parentId: parentId || null }),
...(name && { name }), ...(name && { name }),
...(avatar && { avatar }), ...(avatar && { avatar }),
...(tags && { tags }),
...(permission && { permission }), ...(permission && { permission }),
...(agentModel && { agentModel: agentModel.model }), ...(agentModel && { agentModel: agentModel.model }),
...(websiteConfig && { websiteConfig }), ...(websiteConfig && { websiteConfig }),
...(status && { status }) ...(status && { status }),
...(intro && { intro })
} }
); );

View File

@ -1,51 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import type { InitShareChatResponse } from '@fastgpt/global/support/outLink/api.d';
import { HUMAN_ICON } from '@fastgpt/global/core/chat/constants';
import { getGuideModule } from '@fastgpt/global/core/module/utils';
import { authShareChatInit } from '@/service/support/outLink/auth';
import { getChatModelNameListByModules } from '@/service/core/app/module';
import { authOutLinkValid } from '@fastgpt/service/support/permission/auth/outLink';
/* init share chat window */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
let { shareId, authToken } = req.query as {
shareId: string;
authToken?: string;
};
// get shareChat
const { app, shareChat } = await authOutLinkValid({ shareId });
// 校验使用权限
const [user] = await Promise.all([
MongoUser.findById(shareChat.userId, 'avatar'),
authShareChatInit({
authToken,
tokenUrl: shareChat.limit?.hookUrl
})
]);
jsonRes<InitShareChatResponse>(res, {
data: {
userAvatar: user?.avatar || HUMAN_ICON,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
name: app.name,
avatar: app.avatar,
intro: app.intro
}
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -3,7 +3,7 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoUser } from '@fastgpt/service/support/user/schema';
import { createJWT, setCookie } from '@fastgpt/service/support/permission/controller'; import { createJWT, setCookie } from '@fastgpt/service/support/permission/controller';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { getUserDetail } from '@/service/support/user/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { PostLoginProps } from '@fastgpt/global/support/user/api.d'; import type { PostLoginProps } from '@fastgpt/global/support/user/api.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { getUserDetail } from '@/service/support/user/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {

View File

@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { authCert, authCertAndShareId } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { withNextCors } from '@fastgpt/service/common/middle/cors'; import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { getUploadModel } from '@fastgpt/service/common/file/upload/multer'; import { getUploadModel } from '@fastgpt/service/common/file/upload/multer';
import fs from 'fs'; import fs from 'fs';

View File

@ -10,11 +10,11 @@ import { dispatchModules } from '@/service/moduleDispatch';
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d'; import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d';
import type { ChatMessageItemType } from '@fastgpt/global/core/ai/type.d'; import type { ChatMessageItemType } from '@fastgpt/global/core/ai/type.d';
import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt'; import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory'; import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { saveChat } from '@/service/utils/chat/saveChat'; import { saveChat } from '@/service/utils/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response'; import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatBill } from '@/service/support/wallet/bill/push'; import { pushChatBill } from '@/service/support/wallet/bill/push';
import { authOutLinkChat } from '@/service/support/permission/auth/outLink'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';
import { pushResult2Remote, updateOutLinkUsage } from '@fastgpt/service/support/outLink/tools'; import { pushResult2Remote, updateOutLinkUsage } from '@fastgpt/service/support/outLink/tools';
import requestIp from 'request-ip'; import requestIp from 'request-ip';
import { getBillSourceByAuthType } from '@fastgpt/global/support/wallet/bill/tools'; import { getBillSourceByAuthType } from '@fastgpt/global/support/wallet/bill/tools';
@ -22,9 +22,10 @@ import { getBillSourceByAuthType } from '@fastgpt/global/support/wallet/bill/too
import { selectShareResponse } from '@/utils/service/core/chat'; import { selectShareResponse } from '@/utils/service/core/chat';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools'; import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { getUserAndAuthBalance } from '@/service/support/permission/auth/user'; import { getUserAndAuthBalance } from '@fastgpt/service/support/user/controller';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { autChatCrud } from '@/service/support/permission/auth/chat';
type FastGptWebChatProps = { type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
@ -32,7 +33,7 @@ type FastGptWebChatProps = {
}; };
type FastGptShareChatProps = { type FastGptShareChatProps = {
shareId?: string; shareId?: string;
authToken?: string; outLinkUid?: string;
}; };
export type Props = ChatCompletionCreateParams & export type Props = ChatCompletionCreateParams &
FastGptWebChatProps & FastGptWebChatProps &
@ -56,11 +57,11 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
res.end(); res.end();
}); });
let { const {
chatId, chatId,
appId, appId,
shareId, shareId,
authToken, outLinkUid,
stream = false, stream = false,
detail = false, detail = false,
messages = [], messages = [],
@ -93,22 +94,29 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
throw new Error('Question is empty'); throw new Error('Question is empty');
} }
/* auth app permission */ /* auth app permission */
const { user, app, responseDetail, authType, apikey, canWrite } = await (async () => { const { user, app, responseDetail, authType, apikey, canWrite, uid } = await (async () => {
if (shareId) { if (shareId && outLinkUid) {
const { user, app, authType, responseDetail } = await authOutLinkChat({ const { user, appId, authType, responseDetail, uid } = await authOutLinkChatStart({
shareId, shareId,
ip: requestIp.getClientIp(req), ip: requestIp.getClientIp(req),
authToken, outLinkUid,
question: question.value question: question.value
}); });
const app = await MongoApp.findById(appId);
if (!app) {
return Promise.reject('app is empty');
}
return { return {
user, user,
app, app,
responseDetail, responseDetail,
apikey: '', apikey: '',
authType, authType,
canWrite: false canWrite: false,
uid
}; };
} }
@ -146,11 +154,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}; };
} }
// token auth
if (!appId) { if (!appId) {
return Promise.reject('appId is empty'); return Promise.reject('appId is empty');
} }
// token
const { app, canWrite } = await authApp({ const { app, canWrite } = await authApp({
req, req,
authToken: true, authToken: true,
@ -168,12 +175,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}; };
})(); })();
// auth app, get history // auth chat permission
const { history } = await getChatHistory({ chatId, tmbId: user.team.tmbId }); await autChatCrud({
req,
authToken: true,
authApiKey: true,
chatId,
shareId,
outLinkUid,
per: 'w'
});
const isAppOwner = !shareId && String(user.team.tmbId) === String(app.tmbId); // get and concat history
const { history } = await getChatItems({ chatId, limit: 30, field: `dataId obj value` });
/* format prompts */
const concatHistory = history.concat(chatMessages); const concatHistory = history.concat(chatMessages);
/* start flow controller */ /* start flow controller */
@ -202,8 +216,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
teamId: user.team.teamId, teamId: user.team.teamId,
tmbId: user.team.tmbId, tmbId: user.team.tmbId,
variables, variables,
updateUseTime: isAppOwner, // owner update use time updateUseTime: !shareId && String(user.team.tmbId) === String(app.tmbId), // owner update use time
shareId, shareId,
outLinkUid: uid,
source: (() => { source: (() => {
if (shareId) { if (shareId) {
return ChatSourceEnum.share; return ChatSourceEnum.share;
@ -281,7 +296,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}); });
if (shareId) { if (shareId) {
pushResult2Remote({ authToken, shareId, responseData }); pushResult2Remote({ outLinkUid, shareId, responseData });
updateOutLinkUsage({ updateOutLinkUsage({
shareId, shareId,
total total

View File

@ -1,73 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { connectToDatabase } from '@/service/mongo';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { Types } from '@fastgpt/service/common/mongo';
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
export type Props = {
appId?: string;
chatId?: string;
limit?: number;
};
export type Response = { history: ChatItemType[] };
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { tmbId } = await authCert({ req, authToken: true });
const { chatId, limit } = req.body as Props;
jsonRes<Response>(res, {
data: await getChatHistory({
chatId,
tmbId,
limit
})
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export async function getChatHistory({
chatId,
tmbId,
limit = 30
}: Props & { tmbId: string }): Promise<Response> {
if (!chatId) {
return { history: [] };
}
const history = await MongoChatItem.aggregate([
{
$match: {
chatId,
tmbId: new Types.ObjectId(tmbId)
}
},
{
$sort: {
_id: -1
}
},
{
$limit: limit
},
{
$project: {
dataId: 1,
obj: 1,
value: 1
}
}
]);
history.reverse();
return { history };
}

View File

@ -23,7 +23,7 @@ import { AppLogsListItemType } from '@/types/app';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import ChatBox, { type ComponentRef } from '@/components/ChatBox'; import ChatBox, { type ComponentRef } from '@/components/ChatBox';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getInitChatSiteInfo } from '@/web/core/chat/api'; import { getInitChatInfo } from '@/web/core/chat/api';
import Tag from '@/components/Tag'; import Tag from '@/components/Tag';
import MyModal from '@/components/MyModal'; import MyModal from '@/components/MyModal';
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker'; import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
@ -199,7 +199,7 @@ function DetailLogsModal({
const { data: chat } = useQuery( const { data: chat } = useQuery(
['getChatDetail', chatId], ['getChatDetail', chatId],
() => getInitChatSiteInfo({ appId, chatId }), () => getInitChatInfo({ appId, chatId }),
{ {
onSuccess(res) { onSuccess(res) {
const history = res.history.map((item) => ({ const history = res.history.map((item) => ({

View File

@ -104,7 +104,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
> >
<Flex mb={4} alignItems={'center'}> <Flex mb={4} alignItems={'center'}>
<Avatar src={appDetail.avatar} w={'34px'} borderRadius={'lg'} /> <Avatar src={appDetail.avatar} w={'34px'} borderRadius={'lg'} />
<Box ml={2} fontWeight={'bold'}> <Box ml={2} fontWeight={'bold'} fontSize={'sm'}>
{appDetail.name} {appDetail.name}
</Box> </Box>
</Flex> </Flex>

View File

@ -55,7 +55,7 @@ const ChatHistorySlider = ({
history: HistoryItemType[]; history: HistoryItemType[];
activeChatId: string; activeChatId: string;
onChangeChat: (chatId?: string) => void; onChangeChat: (chatId?: string) => void;
onDelHistory: (chatId: string) => void; onDelHistory: (e: { chatId: string }) => void;
onClearHistory: () => void; onClearHistory: () => void;
onSetHistoryTop?: (e: { chatId: string; top: boolean }) => void; onSetHistoryTop?: (e: { chatId: string; top: boolean }) => void;
onSetCustomTitle?: (e: { chatId: string; title: string }) => void; onSetCustomTitle?: (e: { chatId: string; title: string }) => void;
@ -261,7 +261,7 @@ const ChatHistorySlider = ({
_hover={{ color: 'red.500' }} _hover={{ color: 'red.500' }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelHistory(item.id); onDelHistory({ chatId: item.id });
if (item.id === activeChatId) { if (item.id === activeChatId) {
onChangeChat(); onChangeChat();
} }

View File

@ -1,7 +1,7 @@
import React, { useCallback, useRef } from 'react'; import React, { useCallback, useRef } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getInitChatSiteInfo, delChatRecordById, putChatHistory } from '@/web/core/chat/api'; import { getInitChatInfo, putChatHistory } from '@/web/core/chat/api';
import { import {
Box, Box,
Flex, Flex,
@ -34,6 +34,7 @@ import { serviceSideProps } from '@/web/common/utils/i18n';
import { useAppStore } from '@/web/core/app/store/useAppStore'; import { useAppStore } from '@/web/core/app/store/useAppStore';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils'; import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils'; import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => { const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
const router = useRouter(); const router = useRouter();
@ -49,13 +50,15 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
setLastChatAppId, setLastChatAppId,
lastChatId, lastChatId,
setLastChatId, setLastChatId,
history, histories,
loadHistory, loadHistories,
pushHistory,
updateHistory, updateHistory,
delHistory, delOneHistory,
clearHistory, clearHistories,
chatData, chatData,
setChatData setChatData,
delOneHistoryItem
} = useChatStore(); } = useChatStore();
const { myApps, loadMyApps } = useAppStore(); const { myApps, loadMyApps } = useAppStore();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
@ -85,7 +88,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
prompts[1]?.value?.slice(0, 20) || prompts[1]?.value?.slice(0, 20) ||
'新对话'; '新对话';
// update history // new chat
if (completionChatId !== chatId) { if (completionChatId !== chatId) {
const newHistory: ChatHistoryItemType = { const newHistory: ChatHistoryItemType = {
chatId: completionChatId, chatId: completionChatId,
@ -94,7 +97,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
appId, appId,
top: false top: false
}; };
updateHistory(newHistory); pushHistory(newHistory);
if (controller.signal.reason !== 'leave') { if (controller.signal.reason !== 'leave') {
forbidRefresh.current = true; forbidRefresh.current = true;
router.replace({ router.replace({
@ -105,7 +108,8 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
}); });
} }
} else { } else {
const currentChat = history.find((item) => item.chatId === chatId); // update chat
const currentChat = histories.find((item) => item.chatId === chatId);
currentChat && currentChat &&
updateHistory({ updateHistory({
...currentChat, ...currentChat,
@ -117,30 +121,12 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
setChatData((state) => ({ setChatData((state) => ({
...state, ...state,
title: newTitle, title: newTitle,
history: ChatBoxRef.current?.getChatHistory() || state.history history: ChatBoxRef.current?.getChatHistories() || state.history
})); }));
return { responseText, responseData, isNewChat: forbidRefresh.current }; return { responseText, responseData, isNewChat: forbidRefresh.current };
}, },
[appId, chatId, history, router, setChatData, updateHistory] [appId, chatId, histories, pushHistory, router, setChatData, updateHistory]
);
// del one chat content
const delOneHistoryItem = useCallback(
async ({ contentId, index }: { contentId?: string; index: number }) => {
if (!chatId || !contentId) return;
try {
setChatData((state) => ({
...state,
history: state.history.filter((_, i) => i !== index)
}));
await delChatRecordById({ chatId, contentId });
} catch (err) {
console.log(err);
}
},
[chatId, setChatData]
); );
// get chat app info // get chat app info
@ -156,10 +142,10 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
}) => { }) => {
try { try {
loading && setIsLoading(true); loading && setIsLoading(true);
const res = await getInitChatSiteInfo({ appId, chatId }); const res = await getInitChatInfo({ appId, chatId });
const history = res.history.map((item) => ({ const history = res.history.map((item) => ({
...item, ...item,
status: 'finish' as any status: ChatStatusEnum.finish
})); }));
setChatData({ setChatData({
@ -185,8 +171,13 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
}); });
if (e?.code === 501) { if (e?.code === 501) {
router.replace('/app/list'); router.replace('/app/list');
} else { } else if (chatId) {
router.replace('/chat'); router.replace({
query: {
...router.query,
chatId: ''
}
});
} }
} }
setIsLoading(false); setIsLoading(false);
@ -250,7 +241,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
}); });
}); });
useQuery(['loadHistory', appId], () => (appId ? loadHistory({ appId }) : null)); useQuery(['loadHistories', appId], () => (appId ? loadHistories({ appId }) : null));
return ( return (
<Flex h={'100%'}> <Flex h={'100%'}>
@ -289,7 +280,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
appAvatar={chatData.app.avatar} appAvatar={chatData.app.avatar}
activeChatId={chatId} activeChatId={chatId}
onClose={onCloseSlider} onClose={onCloseSlider}
history={history.map((item, i) => ({ history={histories.map((item, i) => ({
id: item.chatId, id: item.chatId,
title: item.title, title: item.title,
customTitle: item.customTitle, customTitle: item.customTitle,
@ -306,39 +297,24 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
onCloseSlider(); onCloseSlider();
} }
}} }}
onDelHistory={delHistory} onDelHistory={delOneHistory}
onClearHistory={() => { onClearHistory={() => {
clearHistory(appId); clearHistories({ appId });
router.replace({ router.replace({
query: { query: {
appId appId
} }
}); });
}} }}
onSetHistoryTop={async (e) => { onSetHistoryTop={(e) => {
try { updateHistory(e);
await putChatHistory(e);
const historyItem = history.find((item) => item.chatId === e.chatId);
if (!historyItem) return;
updateHistory({
...historyItem,
top: e.top
});
} catch (error) {}
}} }}
onSetCustomTitle={async (e) => { onSetCustomTitle={async (e) => {
try { updateHistory({
putChatHistory({ chatId: e.chatId,
chatId: e.chatId, title: e.title,
customTitle: e.title customTitle: e.title
}); });
const historyItem = history.find((item) => item.chatId === e.chatId);
if (!historyItem) return;
updateHistory({
...historyItem,
customTitle: e.title
});
} catch (error) {}
}} }}
/> />
)} )}
@ -372,7 +348,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
feedbackType={'user'} feedbackType={'user'}
onUpdateVariable={(e) => {}} onUpdateVariable={(e) => {}}
onStartChat={startChat} onStartChat={startChat}
onDelMessage={delOneHistoryItem} onDelMessage={(e) => delOneHistoryItem({ ...e, chatId })}
/> />
</Box> </Box>
</Flex> </Flex>

View File

@ -1,17 +1,16 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { initShareChatInfo } from '@/web/support/outLink/api';
import { Box, Flex, useDisclosure, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react'; import { Box, Flex, useDisclosure, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react';
import { useToast } from '@/web/common/hooks/useToast'; import { useToast } from '@/web/common/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { streamFetch } from '@/web/common/api/fetch'; import { streamFetch } from '@/web/common/api/fetch';
import { useShareChatStore, defaultHistory } from '@/web/core/chat/storeShareChat'; import { useShareChatStore } from '@/web/core/chat/storeShareChat';
import SideBar from '@/components/SideBar'; import SideBar from '@/components/SideBar';
import { gptMessage2ChatType } from '@/utils/adapt'; import { gptMessage2ChatType } from '@/utils/adapt';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import type { ChatSiteItemType } from '@fastgpt/global/core/chat/type.d'; import type { ChatHistoryItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type.d';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12); const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
@ -21,6 +20,13 @@ import ChatHeader from './components/ChatHeader';
import ChatHistorySlider from './components/ChatHistorySlider'; import ChatHistorySlider from './components/ChatHistorySlider';
import { serviceSideProps } from '@/web/common/utils/i18n'; import { serviceSideProps } from '@/web/common/utils/i18n';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils'; import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import { useTranslation } from 'next-i18next';
import { getInitOutLinkChatInfo } from '@/web/core/chat/api';
import { POST } from '@/web/common/api/request';
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
import { useChatStore } from '@/web/core/chat/storeChat';
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
const OutLink = ({ const OutLink = ({
shareId, shareId,
@ -33,6 +39,7 @@ const OutLink = ({
showHistory: '0' | '1'; showHistory: '0' | '1';
authToken?: string; authToken?: string;
}) => { }) => {
const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
@ -43,18 +50,23 @@ const OutLink = ({
const ChatBoxRef = useRef<ComponentRef>(null); const ChatBoxRef = useRef<ComponentRef>(null);
const { const {
shareChatData, localUId,
setShareChatData, shareChatHistory, // abandon
shareChatHistory, clearLocalHistory // abandon
saveChatResponse,
delShareChatHistoryItemById,
delOneShareHistoryByChatId,
delManyShareChatHistoryByShareId
} = useShareChatStore(); } = useShareChatStore();
const history = useMemo( const {
() => shareChatHistory.filter((item) => item.shareId === shareId), histories,
[shareChatHistory, shareId] loadHistories,
); pushHistory,
updateHistory,
delOneHistory,
chatData,
setChatData,
delOneHistoryItem,
clearHistories
} = useChatStore();
const appId = chatData.appId;
const outLinkUid: string = authToken || localUId;
const startChat = useCallback( const startChat = useCallback(
async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => { async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
@ -67,12 +79,55 @@ const OutLink = ({
variables, variables,
shareId, shareId,
chatId: completionChatId, chatId: completionChatId,
authToken outLinkUid
}, },
onMessage: generatingMessage, onMessage: generatingMessage,
abortSignal: controller abortSignal: controller
}); });
const newTitle =
chatContentReplaceBlock(prompts[0].content).slice(0, 20) ||
prompts[1]?.value?.slice(0, 20) ||
'新对话';
// new chat
if (completionChatId !== chatId) {
const newHistory: ChatHistoryItemType = {
chatId: completionChatId,
updateTime: new Date(),
title: newTitle,
appId,
top: false
};
pushHistory(newHistory);
if (controller.signal.reason !== 'leave') {
forbidRefresh.current = true;
router.replace({
query: {
...router.query,
chatId: completionChatId
}
});
}
} else {
// update chat
const currentChat = histories.find((item) => item.chatId === chatId);
currentChat &&
updateHistory({
...currentChat,
updateTime: new Date(),
title: newTitle
});
}
// update chat window
setChatData((state) => ({
...state,
title: newTitle,
history: ChatBoxRef.current?.getChatHistories() || state.history
}));
/* post message to report result */
const result: ChatSiteItemType[] = gptMessage2ChatType(prompts).map((item) => ({ const result: ChatSiteItemType[] = gptMessage2ChatType(prompts).map((item) => ({
...item, ...item,
status: 'finish' status: 'finish'
@ -80,24 +135,6 @@ const OutLink = ({
result[1].value = responseText; result[1].value = responseText;
result[1].responseData = responseData; result[1].responseData = responseData;
/* save chat */
saveChatResponse({
chatId: completionChatId,
prompts: result,
variables,
shareId
});
if (completionChatId !== chatId && controller.signal.reason !== 'leave') {
forbidRefresh.current = true;
router.replace({
query: {
...router.query,
chatId: completionChatId
}
});
}
window.top?.postMessage( window.top?.postMessage(
{ {
type: 'shareChatFinish', type: 'shareChatFinish',
@ -111,61 +148,80 @@ const OutLink = ({
return { responseText, responseData, isNewChat: forbidRefresh.current }; return { responseText, responseData, isNewChat: forbidRefresh.current };
}, },
[authToken, chatId, router, saveChatResponse, shareId] [chatId, shareId, outLinkUid, setChatData, appId, updateHistory, router, histories]
); );
const loadAppInfo = useCallback( const loadChatInfo = useCallback(
async (shareId: string, chatId: string, authToken?: string) => { async (shareId: string, chatId: string) => {
if (!shareId) return null; if (!shareId) return null;
const history = shareChatHistory.find((item) => item.chatId === chatId) || defaultHistory;
ChatBoxRef.current?.resetHistory(history.chats);
ChatBoxRef.current?.resetVariables(history.variables);
try { try {
const chatData = await (async () => { const res = await getInitOutLinkChatInfo({
if (shareChatData.app.name === '') { chatId,
return initShareChatInfo({ shareId,
shareId, outLinkUid: authToken || localUId
authToken });
}); const history = res.history.map((item) => ({
} ...item,
return shareChatData; status: ChatStatusEnum.finish
})(); }));
setShareChatData({ setChatData({
...chatData, ...res,
history history
}); });
ChatBoxRef.current?.resetHistory(history);
ChatBoxRef.current?.resetVariables(res.variables);
if (res.history.length > 0) {
setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto');
}, 500);
}
} catch (e: any) { } catch (e: any) {
toast({ toast({
status: 'error', status: 'error',
title: getErrText(e, '获取应用失败') title: getErrText(e, t('core.shareChat.Init Error'))
}); });
if (e?.code === 501) { if (chatId) {
delManyShareChatHistoryByShareId(shareId); router.replace({
query: {
...router.query,
chatId: ''
}
});
}
if (e?.statusText === OutLinkErrEnum.linkUnInvalid) {
router.replace('/');
} }
} }
if (history.chats.length > 0) { return null;
setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto');
}, 500);
}
return history;
}, },
[delManyShareChatHistoryByShareId, setShareChatData, shareChatData, shareChatHistory, toast] [authToken, localUId, router, setChatData, t, toast]
); );
useQuery(['init', shareId, chatId], () => {
useQuery(['init', shareId, chatId, authToken], () => {
if (forbidRefresh.current) { if (forbidRefresh.current) {
forbidRefresh.current = false; forbidRefresh.current = false;
return null; return null;
} }
return loadAppInfo(shareId, chatId, authToken);
return loadChatInfo(shareId, chatId);
}); });
// load histories
useQuery(['loadHistories', outLinkUid, shareId], () => {
if (shareId && outLinkUid) {
return loadHistories({
shareId,
outLinkUid
});
}
return null;
});
// check is embed
useEffect(() => { useEffect(() => {
if (window !== top) { if (window !== top) {
window.top?.postMessage({ type: 'shareChatReady' }, '*'); window.top?.postMessage({ type: 'shareChatReady' }, '*');
@ -173,10 +229,32 @@ const OutLink = ({
setIdEmbed(window !== top); setIdEmbed(window !== top);
}, []); }, []);
// todo:4.6.4 init: update local chat history, add outLinkUid
useEffect(() => {
const activeHistory = shareChatHistory.filter((item) => !item.delete);
if (!localUId || !shareId || activeHistory.length === 0) return;
(async () => {
try {
await POST('/core/chat/initLocalShareHistoryV464', {
shareId,
outLinkUid: localUId,
chatIds: shareChatHistory.map((item) => item.chatId)
});
clearLocalHistory();
// router.reload();
} catch (error) {
toast({
status: 'warning',
title: getErrText(error, t('core.shareChat.Init Error'))
});
}
})();
}, [clearLocalHistory, localUId, router, shareChatHistory, shareId, t, toast]);
return ( return (
<PageContainer {...(isEmbed ? { p: '0 !important', borderRadius: '0' } : {})}> <PageContainer {...(isEmbed ? { p: '0 !important', borderRadius: '0' } : {})}>
<Head> <Head>
<title>{shareChatData.app.name}</title> <title>{chatData.app.name}</title>
</Head> </Head>
<Flex h={'100%'} flexDirection={['column', 'row']}> <Flex h={'100%'} flexDirection={['column', 'row']}>
{showHistory === '1' {showHistory === '1'
@ -199,12 +277,14 @@ const OutLink = ({
); );
})( })(
<ChatHistorySlider <ChatHistorySlider
appName={shareChatData.app.name} appName={chatData.app.name}
appAvatar={shareChatData.app.avatar} appAvatar={chatData.app.avatar}
activeChatId={chatId} activeChatId={chatId}
history={history.map((item) => ({ history={histories.map((item) => ({
id: item.chatId, id: item.chatId,
title: item.title title: item.title,
customTitle: item.customTitle,
top: item.top
}))} }))}
onClose={onCloseSlider} onClose={onCloseSlider}
onChangeChat={(chatId) => { onChangeChat={(chatId) => {
@ -218,9 +298,9 @@ const OutLink = ({
onCloseSlider(); onCloseSlider();
} }
}} }}
onDelHistory={delOneShareHistoryByChatId} onDelHistory={({ chatId }) => delOneHistory({ chatId, shareId, outLinkUid })}
onClearHistory={() => { onClearHistory={() => {
delManyShareChatHistoryByShareId(shareId); clearHistories({ shareId, outLinkUid });
router.replace({ router.replace({
query: { query: {
...router.query, ...router.query,
@ -228,6 +308,16 @@ const OutLink = ({
} }
}); });
}} }}
onSetHistoryTop={(e) => {
updateHistory(e);
}}
onSetCustomTitle={async (e) => {
updateHistory({
chatId: e.chatId,
title: e.title,
customTitle: e.title
});
}}
/> />
) )
: null} : null}
@ -242,36 +332,24 @@ const OutLink = ({
> >
{/* header */} {/* header */}
<ChatHeader <ChatHeader
appAvatar={shareChatData.app.avatar} appAvatar={chatData.app.avatar}
appName={shareChatData.app.name} appName={chatData.app.name}
history={shareChatData.history.chats} history={chatData.history}
onOpenSlider={onOpenSlider} onOpenSlider={onOpenSlider}
/> />
{/* chat box */} {/* chat box */}
<Box flex={1}> <Box flex={1}>
<ChatBox <ChatBox
active={!!shareChatData.app.name} active={!!chatData.app.name}
ref={ChatBoxRef} ref={ChatBoxRef}
appAvatar={shareChatData.app.avatar} appAvatar={chatData.app.avatar}
userAvatar={shareChatData.userAvatar} userAvatar={chatData.userAvatar}
userGuideModule={shareChatData.app?.userGuideModule} userGuideModule={chatData.app?.userGuideModule}
showFileSelector={checkChatSupportSelectFileByChatModels( showFileSelector={checkChatSupportSelectFileByChatModels(chatData.app.chatModels)}
shareChatData.app.chatModels
)}
feedbackType={'user'} feedbackType={'user'}
onUpdateVariable={(e) => { onUpdateVariable={(e) => {}}
setShareChatData((state) => ({
...state,
history: {
...state.history,
variables: e
}
}));
}}
onStartChat={startChat} onStartChat={startChat}
onDelMessage={({ contentId, index }) => onDelMessage={(e) => delOneHistoryItem({ ...e, chatId })}
delShareChatHistoryItemById({ chatId, contentId, index })
}
/> />
</Box> </Box>
</Flex> </Flex>

View File

@ -189,15 +189,6 @@ export async function searchDatasetData(props: SearchProps) {
}) })
).filter((item) => item.score > similarity); ).filter((item) => item.score > similarity);
// (It's possible that rerank failed) concat rerank results and search results
set = new Set<string>(reRankResults.map((item) => item.id));
embeddingRecallResults.forEach((item) => {
if (!set.has(item.id) && item.score >= similarity) {
reRankResults.push(item);
set.add(item.id);
}
});
return { return {
searchRes: reRankResults.slice(0, limit), searchRes: reRankResults.slice(0, limit),
tokenLen tokenLen
@ -382,7 +373,7 @@ export async function reRankSearchResult({
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return []; return data;
} }
} }
// ------------------ search end ------------------ // ------------------ search end ------------------

View File

@ -3,7 +3,7 @@ import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants'; import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import { RunningModuleItemType } from '@/types/app'; import { RunningModuleItemType } from '@/types/app';
import { ModuleDispatchProps } from '@/types/core/chat/type'; import { ModuleDispatchProps } from '@/types/core/chat/type';
import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleItemType } from '@fastgpt/global/core/module/type'; import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { UserType } from '@fastgpt/global/support/user/type'; import { UserType } from '@fastgpt/global/support/user/type';

View File

@ -48,7 +48,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
answerText, answerText,
responseData: { responseData: {
moduleLogo: plugin.avatar, moduleLogo: plugin.avatar,
price: responseData.reduce((sum, item) => sum + item.price, 0), price: responseData.reduce((sum, item) => sum + (item.price || 0), 0),
runningTime: responseData.reduce((sum, item) => sum + (item.runningTime || 0), 0), runningTime: responseData.reduce((sum, item) => sum + (item.runningTime || 0), 0),
pluginOutput: output?.pluginOutput pluginOutput: output?.pluginOutput
}, },

View File

@ -1,14 +0,0 @@
import { POST } from '@fastgpt/service/common/api/plusRequest';
import type {
AuthLinkLimitProps,
AuthShareChatInitProps
} from '@fastgpt/global/support/outLink/api.d';
export function authOutLinkLimit(data: AuthLinkLimitProps) {
return POST('/support/outLink/authLimit', data);
}
export function authShareChatInit(data: AuthShareChatInitProps) {
if (!global.feConfigs?.isPlus) return;
return POST('/support/outLink/authShareChatInit', data);
}

View File

@ -0,0 +1,59 @@
import { ChatSchema } from '@fastgpt/global/core/chat/type';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { AuthModeType } from '@fastgpt/service/support/permission/type';
import { authOutLink } from './outLink';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { authUserRole } from '@fastgpt/service/support/permission/auth/user';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
/*
outLink: Must be the owner
token: team owner and chat owner have all permissions
*/
export async function autChatCrud({
chatId,
shareId,
outLinkUid,
per = 'owner',
...props
}: AuthModeType & {
chatId?: string;
shareId?: string;
outLinkUid?: string;
}): Promise<{
chat?: ChatSchema;
isOutLink: boolean;
uid?: string;
}> {
const isOutLink = Boolean(shareId && outLinkUid);
if (!chatId) return { isOutLink, uid: outLinkUid };
const chat = await MongoChat.findOne({ chatId }).lean();
if (!chat) return { isOutLink, uid: outLinkUid };
const { uid } = await (async () => {
// outLink Auth
if (shareId && outLinkUid) {
const { uid } = await authOutLink({ shareId, outLinkUid });
// auth outLinkUid
if (chat.shareId === shareId && chat.outLinkUid === uid) {
return { uid };
}
return Promise.reject(ChatErrEnum.unAuthChat);
}
// req auth
const { tmbId, role } = await authUserRole(props);
if (role === TeamMemberRoleEnum.owner) return { uid: outLinkUid };
if (String(tmbId) === String(chat.tmbId)) return { uid: outLinkUid };
return Promise.reject(ChatErrEnum.unAuthChat);
})();
return {
chat,
isOutLink,
uid
};
}

View File

@ -1,31 +1,74 @@
import { authOutLinkLimit } from '@/service/support/outLink/auth'; import { POST } from '@fastgpt/service/common/api/plusRequest';
import { AuthLinkChatProps } from '@fastgpt/global/support/outLink/api.d'; import type {
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; AuthOutLinkChatProps,
import { getUserAndAuthBalance } from './user'; AuthOutLinkLimitProps,
AuthOutLinkInitProps,
AuthOutLinkResponse
} from '@fastgpt/global/support/outLink/api.d';
import { authOutLinkValid } from '@fastgpt/service/support/permission/auth/outLink'; import { authOutLinkValid } from '@fastgpt/service/support/permission/auth/outLink';
import { getUserAndAuthBalance } from '@fastgpt/service/support/user/controller';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
export async function authOutLinkChat({ export function authOutLinkInit(data: AuthOutLinkInitProps): Promise<AuthOutLinkResponse> {
if (!global.feConfigs?.isPlus) return Promise.resolve({ uid: data.outLinkUid });
return POST<AuthOutLinkResponse>('/support/outLink/authInit', data);
}
export function authOutLinkChatLimit(data: AuthOutLinkLimitProps): Promise<AuthOutLinkResponse> {
if (!global.feConfigs?.isPlus) return Promise.resolve({ uid: data.outLinkUid });
return POST<AuthOutLinkResponse>('/support/outLink/authChatStart', data);
}
export const authOutLink = async ({
shareId,
outLinkUid
}: {
shareId?: string;
outLinkUid?: string;
}): Promise<{
uid: string;
appId: string;
shareChat: OutLinkSchema;
}> => {
if (!outLinkUid) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
const result = await authOutLinkValid({ shareId });
const { uid } = await authOutLinkInit({
outLinkUid,
tokenUrl: result.shareChat.limit?.hookUrl
});
return {
...result,
uid
};
};
export async function authOutLinkChatStart({
shareId, shareId,
ip, ip,
authToken, outLinkUid,
question question
}: AuthLinkChatProps & { }: AuthOutLinkChatProps & {
shareId: string; shareId: string;
}) { }) {
// get outLink // get outLink and app
const { shareChat, app } = await authOutLinkValid({ shareId }); const { shareChat, appId } = await authOutLinkValid({ shareId });
const [user] = await Promise.all([ // check balance and chat limit
const [user, { uid }] = await Promise.all([
getUserAndAuthBalance({ tmbId: shareChat.tmbId, minBalance: 0 }), getUserAndAuthBalance({ tmbId: shareChat.tmbId, minBalance: 0 }),
global.feConfigs?.isPlus authOutLinkChatLimit({ outLink: shareChat, ip, outLinkUid, question })
? authOutLinkLimit({ outLink: shareChat, ip, authToken, question })
: undefined
]); ]);
return { return {
authType: AuthUserTypeEnum.token, authType: AuthUserTypeEnum.token,
responseDetail: shareChat.responseDetail, responseDetail: shareChat.responseDetail,
user, user,
app appId,
uid
}; };
} }

View File

@ -1,46 +0,0 @@
import { AuthResponseType } from '@fastgpt/global/support/permission/type';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
import { AuthModeType } from '@fastgpt/service/support/permission/type';
import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
import { UserType } from '@fastgpt/global/support/user/type';
import { getUserDetail } from '@/service/support/user/controller';
export async function getUserAndAuthBalance({
tmbId,
minBalance
}: {
tmbId: string;
minBalance?: number;
}) {
const user = await getUserDetail({ tmbId });
if (!user) {
return Promise.reject(UserErrEnum.unAuthUser);
}
if (minBalance !== undefined && global.feConfigs.isPlus && user.team.balance < minBalance) {
return Promise.reject(UserErrEnum.balanceNotEnough);
}
return user;
}
/* get user */
export async function authUser({
minBalance,
...props
}: AuthModeType & {
minBalance?: number;
}): Promise<
AuthResponseType & {
user: UserType;
}
> {
const result = await parseHeaderCert(props);
return {
...result,
user: await getUserAndAuthBalance({ tmbId: result.tmbId, minBalance }),
isOwner: true,
canWrite: true
};
}

View File

@ -1,41 +0,0 @@
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { UserType } from '@fastgpt/global/support/user/type';
import {
getTeamInfoByTmbId,
getUserDefaultTeam
} from '@fastgpt/service/support/user/team/controller';
export async function getUserDetail({
tmbId,
userId
}: {
tmbId?: string;
userId?: string;
}): Promise<UserType> {
const team = await (async () => {
if (tmbId) {
return getTeamInfoByTmbId({ tmbId });
}
if (userId) {
return getUserDefaultTeam({ userId });
}
return Promise.reject(ERROR_ENUM.unAuthorization);
})();
const user = await MongoUser.findById(team.userId);
if (!user) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}
return {
_id: user._id,
username: user.username,
avatar: user.avatar,
balance: user.balance,
timezone: user.timezone,
promotionRate: user.promotionRate,
openaiAccount: user.openaiAccount,
team
};
}

View File

@ -1,6 +1,6 @@
import { BillSourceEnum, PRICE_SCALE } from '@fastgpt/global/support/wallet/bill/constants'; import { BillSourceEnum, PRICE_SCALE } from '@fastgpt/global/support/wallet/bill/constants';
import { getAudioSpeechModel, getQAModel } from '@/service/core/ai/model'; import { getAudioSpeechModel, getQAModel } from '@/service/core/ai/model';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api.d'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { formatPrice } from '@fastgpt/global/support/wallet/bill/tools'; import { formatPrice } from '@fastgpt/global/support/wallet/bill/tools';
import { addLog } from '@fastgpt/service/common/mongo/controller'; import { addLog } from '@fastgpt/service/common/mongo/controller';
import type { ConcatBillProps, CreateBillProps } from '@fastgpt/global/support/wallet/bill/api.d'; import type { ConcatBillProps, CreateBillProps } from '@fastgpt/global/support/wallet/bill/api.d';
@ -41,7 +41,7 @@ export const pushChatBill = ({
source: `${BillSourceEnum}`; source: `${BillSourceEnum}`;
response: ChatHistoryItemResType[]; response: ChatHistoryItemResType[];
}) => { }) => {
const total = response.reduce((sum, item) => sum + item.price, 0); const total = response.reduce((sum, item) => sum + (item.price || 0), 0);
createBill({ createBill({
teamId, teamId,

View File

@ -15,6 +15,7 @@ type Props = {
updateUseTime: boolean; updateUseTime: boolean;
source: `${ChatSourceEnum}`; source: `${ChatSourceEnum}`;
shareId?: string; shareId?: string;
outLinkUid?: string;
content: [ChatItemType, ChatItemType]; content: [ChatItemType, ChatItemType];
}; };
@ -27,10 +28,11 @@ export async function saveChat({
updateUseTime, updateUseTime,
source, source,
shareId, shareId,
outLinkUid,
content content
}: Props) { }: Props) {
try { try {
const chatHistory = await MongoChat.findOne( const chat = await MongoChat.findOne(
{ {
chatId, chatId,
teamId, teamId,
@ -57,7 +59,7 @@ export async function saveChat({
content[1]?.value?.slice(0, 20) || content[1]?.value?.slice(0, 20) ||
'Chat'; 'Chat';
if (chatHistory) { if (chat) {
promise.push( promise.push(
MongoChat.updateOne( MongoChat.updateOne(
{ chatId }, { chatId },
@ -77,7 +79,8 @@ export async function saveChat({
variables, variables,
title, title,
source, source,
shareId shareId,
outLinkUid
}) })
); );
} }

View File

@ -1,11 +1,16 @@
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api.d'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
export function selectShareResponse({ responseData }: { responseData: ChatHistoryItemResType[] }) { export function selectShareResponse({
responseData = []
}: {
responseData?: ChatHistoryItemResType[];
}) {
const filedList = [ const filedList = [
'moduleType', 'moduleType',
'moduleName', 'moduleName',
'moduleLogo', 'moduleLogo',
'runningTime', 'runningTime',
'historyPreview',
'quoteList', 'quoteList',
'question' 'question'
]; ];
@ -17,6 +22,6 @@ export function selectShareResponse({ responseData }: { responseData: ChatHistor
obj[key] = item[key]; obj[key] = item[key];
} }
} }
return obj; return obj as ChatHistoryItemResType;
}); });
} }

View File

@ -1,7 +1,7 @@
import { sseResponseEventEnum } from '@fastgpt/service/common/response/constant'; import { sseResponseEventEnum } from '@fastgpt/service/common/response/constant';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { parseStreamChunk, SSEParseData } from '@/utils/sse'; import { parseStreamChunk, SSEParseData } from '@/utils/sse';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/api.d'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { StartChatFnProps } from '@/components/ChatBox'; import { StartChatFnProps } from '@/components/ChatBox';
import { getToken } from '@/web/support/user/auth'; import { getToken } from '@/web/support/user/auth';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants'; import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';

View File

@ -346,7 +346,7 @@ export const appTemplates: (AppItemType & {
{ {
key: 'similarity', key: 'similarity',
type: 'slider', type: 'slider',
label: '相度', label: '相度',
value: 0.4, value: 0.4,
min: 0, min: 0,
max: 1, max: 1,
@ -1398,7 +1398,7 @@ export const appTemplates: (AppItemType & {
{ {
key: 'similarity', key: 'similarity',
type: 'slider', type: 'slider',
label: '相度', label: '相度',
value: 0.76, value: 0.76,
min: 0, min: 0,
max: 1, max: 1,

View File

@ -1,42 +1,53 @@
import { GET, POST, DELETE, PUT } from '@/web/common/api/request'; import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d'; import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
import type { InitChatResponse } from '@fastgpt/global/core/chat/api.d'; import type {
import type { RequestPaging } from '@/types'; InitChatProps,
import { UpdateHistoryProps } from '@fastgpt/global/core/chat/api.d'; InitChatResponse,
import type { AdminUpdateFeedbackParams } from '@fastgpt/global/core/chat/api.d'; InitOutLinkChatProps,
import { GetChatSpeechProps } from '@/global/core/chat/api.d'; getHistoriesProps
} from '@/global/core/chat/api.d';
import type {
AdminUpdateFeedbackParams,
ClearHistoriesProps,
DelHistoryProps,
DeleteChatItemProps,
UpdateHistoryProps
} from '@/global/core/chat/api.d';
/** /**
* *
*/ */
export const getInitChatSiteInfo = (data: { appId: string; chatId?: string }) => export const getInitChatInfo = (data: InitChatProps) =>
GET<InitChatResponse>(`/core/chat/init`, data); GET<InitChatResponse>(`/core/chat/init`, data);
export const getInitOutLinkChatInfo = (data: InitOutLinkChatProps) =>
GET<InitChatResponse>(`/core/chat/outLink/init`, data);
/** /**
* * get current window history(appid or shareId)
*/ */
export const getChatHistory = (data: RequestPaging & { appId: string }) => export const getChatHistories = (data: getHistoriesProps) =>
POST<ChatHistoryItemType[]>('/core/chat/list', data); POST<ChatHistoryItemType[]>('/core/chat/getHistories', data);
/** /**
* * delete one history
*/ */
export const delChatHistoryById = (chatId: string) => DELETE(`/core/chat/delete`, { chatId }); export const delChatHistoryById = (data: DelHistoryProps) => DELETE(`/core/chat/delHistory`, data);
/** /**
* clear all history by appid * clear all history by appid
*/ */
export const clearChatHistoryByAppId = (appId: string) => DELETE(`/core/chat/delete`, { appId }); export const clearChatHistoryByAppId = (data: ClearHistoriesProps) =>
DELETE(`/core/chat/clearHistories`, data);
/** /**
* * delete one chat record
*/ */
export const delChatRecordById = (data: { chatId: string; contentId: string }) => export const delChatRecordById = (data: DeleteChatItemProps) =>
DELETE(`/core/chat/item/delete`, data); DELETE(`/core/chat/item/delete`, data);
/** /**
* 修改历史记录: 标题/ * 修改历史记录: 标题/
*/ */
export const putChatHistory = (data: UpdateHistoryProps) => PUT('/core/chat/update', data); export const putChatHistory = (data: UpdateHistoryProps) => PUT('/core/chat/updateHistory', data);
export const userUpdateChatFeedback = (data: { chatItemId: string; userFeedback?: string }) => export const userUpdateChatFeedback = (data: { chatItemId: string; userFeedback?: string }) =>
POST('/core/chat/feedback/userUpdate', data); POST('/core/chat/feedback/userUpdate', data);

View File

@ -2,35 +2,36 @@ import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d'; import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
import type { InitChatResponse } from '@fastgpt/global/core/chat/api'; import type {
import { delChatHistoryById, getChatHistory, clearChatHistoryByAppId } from '@/web/core/chat/api'; InitChatResponse,
getHistoriesProps,
ClearHistoriesProps,
DelHistoryProps,
UpdateHistoryProps
} from '@/global/core/chat/api';
import {
delChatHistoryById,
getChatHistories,
clearChatHistoryByAppId,
delChatRecordById,
putChatHistory
} from '@/web/core/chat/api';
import { defaultChatData } from '@/global/core/chat/constants';
type State = { type State = {
history: ChatHistoryItemType[]; histories: ChatHistoryItemType[];
loadHistory: (data: { appId: string }) => Promise<null>; loadHistories: (data: getHistoriesProps) => Promise<null>;
delHistory(history: string): Promise<void>; delOneHistory(data: DelHistoryProps): Promise<void>;
clearHistory(appId: string): Promise<void>; clearHistories(data: ClearHistoriesProps): Promise<void>;
updateHistory: (history: ChatHistoryItemType) => void; pushHistory: (history: ChatHistoryItemType) => void;
updateHistory: (e: UpdateHistoryProps & { updateTime?: Date; title?: string }) => Promise<any>;
chatData: InitChatResponse; chatData: InitChatResponse;
setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void; setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void;
lastChatAppId: string; lastChatAppId: string;
setLastChatAppId: (id: string) => void; setLastChatAppId: (id: string) => void;
lastChatId: string; lastChatId: string;
setLastChatId: (id: string) => void; setLastChatId: (id: string) => void;
}; delOneHistoryItem: (e: { chatId: string; contentId?: string; index: number }) => Promise<any>;
const defaultChatData: InitChatResponse = {
chatId: '',
appId: '',
app: {
name: 'Loading',
avatar: '/icon/logo.svg',
intro: '',
canUse: false
},
title: '新对话',
variables: {},
history: []
}; };
export const useChatStore = create<State>()( export const useChatStore = create<State>()(
@ -49,49 +50,62 @@ export const useChatStore = create<State>()(
state.lastChatId = id; state.lastChatId = id;
}); });
}, },
history: [], histories: [],
async loadHistory({ appId }) { async loadHistories(e) {
const oneHistory = get().history[0]; const data = await getChatHistories(e);
if (oneHistory && oneHistory.appId === appId) return null;
const data = await getChatHistory({
appId,
pageNum: 1,
pageSize: 20
});
set((state) => { set((state) => {
state.history = data; state.histories = data;
}); });
return null; return null;
}, },
async delHistory(chatId) { async delOneHistory(props) {
set((state) => { set((state) => {
state.history = state.history.filter((item) => item.chatId !== chatId); state.histories = state.histories.filter((item) => item.chatId !== props.chatId);
}); });
await delChatHistoryById(chatId); await delChatHistoryById(props);
}, },
async clearHistory(appId) { async clearHistories(data) {
set((state) => { set((state) => {
state.history = []; state.histories = [];
}); });
await clearChatHistoryByAppId(appId); await clearChatHistoryByAppId(data);
}, },
updateHistory(history) { pushHistory(history) {
const index = get().history.findIndex((item) => item.chatId === history.chatId);
set((state) => { set((state) => {
const newHistory = (() => { state.histories = [history, ...state.histories];
if (index > -1) { });
return [ },
history, async updateHistory(props) {
...get().history.slice(0, index), const { chatId, customTitle, top, title, updateTime } = props;
...get().history.slice(index + 1) const index = get().histories.findIndex((item) => item.chatId === chatId);
];
} else {
return [history, ...state.history];
}
})();
state.history = newHistory; if (index > -1) {
}); const newHistory = {
...get().histories[index],
...(title && { title }),
...(updateTime && { updateTime }),
...(customTitle !== undefined && { customTitle }),
...(top !== undefined && { top })
};
if (customTitle !== undefined || top !== undefined) {
try {
putChatHistory(props);
} catch (error) {}
}
set((state) => {
const newHistories = (() => {
return [
newHistory,
...get().histories.slice(0, index),
...get().histories.slice(index + 1)
];
})();
state.histories = newHistories;
});
}
}, },
chatData: defaultChatData, chatData: defaultChatData,
setChatData(e = defaultChatData) { setChatData(e = defaultChatData) {
@ -104,6 +118,19 @@ export const useChatStore = create<State>()(
state.chatData = e; state.chatData = e;
}); });
} }
},
async delOneHistoryItem({ chatId, contentId, index }) {
if (!chatId || !contentId) return;
try {
get().setChatData((state) => ({
...state,
history: state.history.filter((_, i) => i !== index)
}));
await delChatRecordById({ chatId, contentId });
} catch (err) {
console.log(err);
}
} }
})), })),
{ {

View File

@ -1,142 +1,39 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import type { import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
ShareChatHistoryItemType, import { customAlphabet } from 'nanoid';
ShareChatType const nanoid = customAlphabet(
} from '@fastgpt/global/support/outLink/api.d'; 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWSYZ1234567890_',
import type { ChatSiteItemType } from '@fastgpt/global/core/chat/type.d'; 24
import { HUMAN_ICON } from '@fastgpt/global/core/chat/constants'; );
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
type State = { type State = {
shareChatData: ShareChatType; localUId: string;
setShareChatData: (e: ShareChatType | ((e: ShareChatType) => ShareChatType)) => void; shareChatHistory: (ChatHistoryItemType & { delete?: boolean })[];
shareChatHistory: ShareChatHistoryItemType[]; clearLocalHistory: (shareId?: string) => void;
saveChatResponse: (e: {
chatId: string;
prompts: ChatSiteItemType[];
variables: Record<string, any>;
shareId: string;
}) => void;
delOneShareHistoryByChatId: (chatId: string) => void;
delShareChatHistoryItemById: (e: { chatId: string; contentId?: string; index: number }) => void;
delManyShareChatHistoryByShareId: (shareId?: string) => void;
};
export const defaultHistory: ShareChatHistoryItemType = {
chatId: `${Date.now()}`,
updateTime: new Date(),
title: '新对话',
shareId: '',
chats: []
};
const defaultShareChatData: ShareChatType = {
userAvatar: HUMAN_ICON,
app: {
name: '',
avatar: '/icon/logo.svg',
intro: ''
},
history: defaultHistory
}; };
export const useShareChatStore = create<State>()( export const useShareChatStore = create<State>()(
devtools( devtools(
persist( persist(
immer((set, get) => ({ immer((set, get) => ({
shareChatData: defaultShareChatData, localUId: `shareChat-${Date.now()}-${nanoid()}`,
setShareChatData(e) { shareChatHistory: [], // old version field
const val = (() => { clearLocalHistory() {
if (typeof e === 'function') { // abandon
return e(get().shareChatData);
} else {
return e;
}
})();
set((state) => { set((state) => {
state.shareChatData = val; state.shareChatHistory = state.shareChatHistory.map((item) => ({
// update history ...item,
state.shareChatHistory = state.shareChatHistory.map((item) => delete: true
item.chatId === val.history.chatId ? val.history : item }));
);
});
},
shareChatHistory: [],
saveChatResponse({ chatId, prompts, variables, shareId }) {
const chatHistory = get().shareChatHistory.find((item) => item.chatId === chatId);
const newTitle =
chatContentReplaceBlock(prompts[prompts.length - 2]?.value).slice(0, 20) ||
prompts[prompts.length - 1]?.value?.slice(0, 20) ||
'Chat';
const historyList = (() => {
if (chatHistory) {
return get().shareChatHistory.map((item) =>
item.chatId === chatId
? {
...item,
title: newTitle,
updateTime: new Date(),
chats: chatHistory.chats.concat(prompts).slice(-30),
variables
}
: item
);
}
return get().shareChatHistory.concat({
chatId,
shareId,
title: newTitle,
updateTime: new Date(),
chats: prompts,
variables
});
})();
// @ts-ignore
historyList.sort((a, b) => new Date(b.updateTime) - new Date(a.updateTime));
set((state) => {
state.shareChatHistory = historyList.slice(0, 50);
});
},
delOneShareHistoryByChatId(chatId: string) {
set((state) => {
state.shareChatHistory = state.shareChatHistory.filter(
(item) => item.chatId !== chatId
);
});
},
delShareChatHistoryItemById({ chatId, contentId }) {
set((state) => {
// update history store
const newHistoryList = state.shareChatHistory.map((item) =>
item.chatId === chatId
? {
...item,
chats: item.chats.filter((item) => item.dataId !== contentId)
}
: item
);
state.shareChatHistory = newHistoryList;
});
},
delManyShareChatHistoryByShareId(shareId?: string) {
set((state) => {
if (shareId) {
state.shareChatHistory = state.shareChatHistory.filter(
(item) => item.shareId !== shareId
);
} else {
state.shareChatHistory = [];
}
}); });
} }
})), })),
{ {
name: 'shareChatStore', name: 'shareChatStore',
partialize: (state) => ({ partialize: (state) => ({
localUId: state.localUId,
shareChatHistory: state.shareChatHistory shareChatHistory: state.shareChatHistory
}) })
} }

View File

@ -79,8 +79,7 @@ export const useDatasetStore = create<State>()(
item._id === data.id item._id === data.id
? { ? {
...item, ...item,
...data, ...data
tags: data.tags || []
} }
: item : item
); );

View File

@ -1,13 +1,6 @@
import { GET, POST, DELETE } from '@/web/common/api/request'; import { GET, POST, DELETE } from '@/web/common/api/request';
import type { InitShareChatResponse } from '@fastgpt/global/support/outLink/api.d';
import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type.d'; import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type.d';
/**
*
*/
export const initShareChatInfo = (data: { shareId: string; authToken?: string }) =>
GET<InitShareChatResponse>(`/support/outLink/init`, data);
/** /**
* create a shareChat * create a shareChat
*/ */

View File

@ -1,7 +1,5 @@
import { GET } from '@/web/common/api/request'; import { GET } from '@/web/common/api/request';
import type { PaySchema } from '@fastgpt/global/support/wallet/pay/type.d'; import type { PaySchema } from '@fastgpt/global/support/wallet/pay/type.d';
import { delay } from '@fastgpt/global/common/system/utils';
export const getPayOrders = () => GET<PaySchema[]>(`/plusApi/support/wallet/pay/getPayOrders`); export const getPayOrders = () => GET<PaySchema[]>(`/plusApi/support/wallet/pay/getPayOrders`);
export const getPayCode = (amount: number) => export const getPayCode = (amount: number) =>
@ -11,15 +9,9 @@ export const getPayCode = (amount: number) =>
}>(`/plusApi/support/wallet/pay/getPayCode`, { amount }); }>(`/plusApi/support/wallet/pay/getPayCode`, { amount });
export const checkPayResult = (payId: string) => export const checkPayResult = (payId: string) =>
GET<number>(`/plusApi/support/wallet/pay/checkPayResult`, { payId }).then(() => { GET<string>(`/plusApi/support/wallet/pay/checkPayResult`, { payId }).then((data) => {
async function startQueue() { try {
try { GET('/common/system/unlockTask');
await GET('/common/system/unlockTask'); } catch (error) {}
} catch (error) { return data;
await delay(1000);
startQueue();
}
}
startQueue();
return 'success';
}); });