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
80 changed files with 1401 additions and 1109 deletions

View File

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

View File

@@ -4,7 +4,9 @@ import { ErrType } from '../errorCode';
export enum OutLinkErrEnum {
unExist = 'unExist',
unAuthLink = 'unAuthLink',
linkUnInvalid = 'linkUnInvalid'
linkUnInvalid = 'linkUnInvalid',
unAuthUser = 'unAuthUser'
}
const errList = [
{
@@ -19,6 +21,10 @@ const errList = [
code: 501,
statusText: OutLinkErrEnum.linkUnInvalid,
message: '分享链接无效'
},
{
statusText: OutLinkErrEnum.unAuthUser,
message: '身份校验失败'
}
];
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 LOGO_ICON = `/icon/logo.svg`;

View File

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

View File

@@ -1,27 +1,12 @@
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';
export type InitShareChatResponse = {
userAvatar: 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;
export type AuthOutLinkInitProps = {
outLinkUid: 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 = {
_id: string;
shareId: string;
userId: string;
teamId: string;
tmbId: string;
appId: string;

View File

@@ -45,4 +45,4 @@ export const TeamMemberStatusMap = {
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 { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant';
import type { UserModelSchema } from '../type';
import type { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant';
export type TeamSchema = {
_id: string;
@@ -22,6 +22,16 @@ export type TeamMemberSchema = {
defaultTeam: boolean;
};
export type TeamMemberWithUserSchema = TeamMemberSchema & {
userId: UserModelSchema;
};
export type TeamMemberWithTeamSchema = TeamMemberSchema & {
teamId: TeamSchema;
};
export type TeamMemberWithTeamAndUserSchema = TeamMemberWithTeamSchema & {
userId: UserModelSchema;
};
export type TeamItemType = {
userId: string;
teamId: string;

View File

@@ -26,9 +26,13 @@ export async function connectMongo({
bufferCommands: true,
maxConnecting: 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,
waitQueueTimeoutMS: 60000
waitQueueTimeoutMS: 60000,
socketTimeoutMS: 60000,
maxIdleTimeMS: 300000,
retryWrites: true,
retryReads: true
});
console.log('mongo connected');

View File

@@ -50,10 +50,6 @@ const ChatSchema = new Schema({
top: {
type: Boolean
},
variables: {
type: Object,
default: {}
},
source: {
type: String,
enum: Object.keys(ChatSourceMap),
@@ -62,9 +58,17 @@ const ChatSchema = new Schema({
shareId: {
type: String
},
isInit: {
type: Boolean,
default: false
outLinkUid: {
type: String
},
variables: {
type: Object,
default: {}
},
metadata: {
//For special storage
type: Object,
default: {}
},
content: {
type: [
@@ -89,9 +93,10 @@ const ChatSchema = new Schema({
});
try {
ChatSchema.index({ tmbId: 1 });
ChatSchema.index({ updateTime: -1 });
ChatSchema.index({ appId: 1 });
ChatSchema.index({ tmbId: 1 });
ChatSchema.index({ shareId: 1 });
ChatSchema.index({ updateTime: -1 });
} catch (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,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: 'user'
},
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,

View File

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

View File

@@ -19,6 +19,7 @@ export async function authApp({
AuthResponseType & {
teamOwner: boolean;
app: AppDetailType;
role: `${TeamMemberRoleEnum}`;
}
> {
const result = await parseHeaderCert(props);
@@ -65,6 +66,7 @@ export async function authApp({
return {
...result,
app,
role,
isOwner,
canWrite,
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);
}
const { app } = await authOutLinkValid({ shareId });
const { shareChat } = await authOutLinkValid({ shareId });
return {
teamId: String(app.teamId),
tmbId: String(app.tmbId),
teamId: String(shareChat.teamId),
tmbId: String(shareChat.tmbId),
authType: AuthUserTypeEnum.outLink,
apikey: '',
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 }) {
if (!shareId) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
const shareChat = await MongoOutLink.findOne({ shareId });
if (!shareChat) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
const app = await MongoApp.findById(shareChat.appId);
if (!app) {
return Promise.reject(AppErrEnum.unExist);
}
return {
app,
appId: shareChat.appId,
shareChat
};
}

View File

@@ -1,4 +1,8 @@
import { UserType } from '@fastgpt/global/support/user/type';
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 }) {
if (userId) {
@@ -9,3 +13,56 @@ export async function authUserExist({ userId, username }: { userId?: string; use
}
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 { connectionMongo, Types } from '../../../common/mongo';
import { TeamItemType, TeamMemberWithTeamSchema } from '@fastgpt/global/support/user/team/type';
import { Types } from '../../../common/mongo';
import {
TeamMemberRoleEnum,
TeamMemberStatusEnum,
TeamCollectionName,
TeamMemberCollectionName,
leaveStatus
notLeaveStatus
} from '@fastgpt/global/support/user/team/constant';
import { MongoTeamMember } from './teamMemberSchema';
import { MongoTeam } from './teamSchema';
async function getTeam(match: Record<string, any>): Promise<TeamItemType> {
const db = connectionMongo?.connection?.db;
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];
const tmb = (await MongoTeamMember.findOne(match).populate('teamId')) as TeamMemberWithTeamSchema;
if (!tmb) {
return Promise.reject('member not exist');
@@ -37,17 +17,17 @@ async function getTeam(match: Record<string, any>): Promise<TeamItemType> {
return {
userId: String(tmb.userId),
teamId: String(tmb.teamId),
teamName: tmb.team.name,
teamId: String(tmb.teamId._id),
teamName: tmb.teamId.name,
memberName: tmb.name,
avatar: tmb.team.avatar,
balance: tmb.team.balance,
avatar: tmb.teamId.avatar,
balance: tmb.teamId.balance,
tmbId: String(tmb._id),
role: tmb.role,
status: tmb.status,
defaultTeam: tmb.defaultTeam,
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({
_id: new Types.ObjectId(tmbId),
status: leaveStatus
status: notLeaveStatus
});
}
@@ -83,12 +63,8 @@ export async function createDefaultTeam({
balance?: number;
maxSize?: number;
}) {
const db = connectionMongo.connection.db;
const Team = db.collection(TeamCollectionName);
const TeamMember = db.collection(TeamMemberCollectionName);
// auth default team
const tmb = await TeamMember.findOne({
const tmb = await MongoTeamMember.findOne({
userId: new Types.ObjectId(userId),
defaultTeam: true
});
@@ -97,7 +73,7 @@ export async function createDefaultTeam({
console.log('create default team', userId);
// create
const { insertedId } = await Team.insertOne({
const { _id: insertedId } = await MongoTeam.create({
ownerId: userId,
name: teamName,
avatar,
@@ -105,7 +81,7 @@ export async function createDefaultTeam({
maxSize,
createTime: new Date()
});
await TeamMember.insertOne({
await MongoTeamMember.create({
teamId: insertedId,
userId,
name: 'Owner',
@@ -116,16 +92,11 @@ export async function createDefaultTeam({
});
} else {
console.log('default team exist', userId);
await Team.updateOne(
{
_id: new Types.ObjectId(tmb.teamId)
},
{
await MongoTeam.findByIdAndUpdate(tmb.teamId, {
$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",
"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 Collection": "Data Sync",
"Website Create Success": "Created successfully, data is being synchronized",
"Website Empty Tip": "No associated website yet",
"Website Link": "Website Link",
"Website Sync": "Website",
"id": "Id",
"Sync Collection": "Data Sync",
"metadata": {
"Chunk Size": "Chunk Size",
"Createtime": "Create Time",
@@ -510,6 +510,10 @@
"variable options": "Options"
},
"variable add option": "Add Option"
},
"shareChat": {
"Init Error": "Init Chat Error",
"Init History Error": "Init History Error"
}
},
"dataset": {

View File

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

View File

@@ -1,5 +1,5 @@
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 { Flex, BoxProps, useDisclosure, Image, useTheme, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';

View File

@@ -1,6 +1,6 @@
import React, { useMemo, useState } from '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 { moduleTemplatesFlat } from '@/web/core/modules/template/system';
import Tabs from '../Tabs';

View File

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

View File

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

View File

@@ -1,7 +1,75 @@
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 = {
ttsConfig: AppTTSConfigType;
input: 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. 优先使用知识库内容回答问题。
2. 你可以回答我不知道
3. 不要提及你是从知识库获取的知识
4. 知识库包含 markdown 内容时,按 markdown 格式返回。
2. 不要提及你是从知识库获取的知识
3. 知识库包含 markdown 内容时,按 markdown 格式返回
我的问题是:"{{question}}"`
},
{
@@ -47,9 +46,8 @@ export const Prompt_QuotePromptList: PromptTemplateItem[] = [
"""
回答要求:
1. 优先使用知识库内容回答问题,其中 instruction 是相关介绍output 是预期回答或补充。
2. 你可以回答我不知道
3. 不要提及你是从知识库获取的知识
4. 知识库包含 markdown 内容时,按 markdown 格式返回。
2. 不要提及你是从知识库获取的知识
3. 知识库包含 markdown 内容时,按 markdown 格式返回
我的问题是:"{{question}}"`
},
{

View File

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

View File

@@ -1,32 +1,14 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema';
import { startQueue } from '@/service/utils/tools';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authCert({ req, authToken: true });
await unlockTask(userId);
await authCert({ req, authToken: true });
startQueue();
} catch (error) {}
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 { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
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>) {
@@ -20,6 +21,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await authApp({ req, authToken: true, appId, per: 'owner' });
// 删除对应的聊天
await MongoChatItem.deleteMany({
appId
});
await MongoChat.deleteMany({
appId
});

View File

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

View File

@@ -289,7 +289,7 @@ function datasetTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
key: 'similarity',
value: formData.dataset.similarity,
type: FlowNodeInputTypeEnum.slider,
label: '相度',
label: '相度',
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 type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authUser } from '@/service/support/permission/auth/user';
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 = {
history: ChatItemType[];
@@ -40,15 +41,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
/* user auth */
const [{ teamId, tmbId }, { user }] = await Promise.all([
const [_, { teamId, tmbId }] = await Promise.all([
authApp({ req, authToken: true, appId, per: 'r' }),
authUser({
authCert({
req,
authToken: true,
minBalance: 0
authToken: true
})
]);
// auth balance
const user = await getUserAndAuthBalance({
tmbId,
minBalance: 0
});
/* start process */
const { responseData } = await dispatchModules({
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 { connectToDatabase } from '@/service/mongo';
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';
/* 初始化我的聊天框,需要身份验证 */

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 { jsonRes } from '@fastgpt/service/common/response';
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 { getGuideModule } from '@fastgpt/global/core/module/utils';
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 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) {
try {
await connectToDatabase();
let { appId, chatId } = req.query as {
appId: string;
chatId: '' | string;
};
let { appId, chatId } = req.query as InitChatProps;
if (!appId) {
return jsonRes(res, {
@@ -27,57 +23,44 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
// 校验使用权限
const [{ app }, autChatResult] = await Promise.all([
// auth app permission
const [{ app, tmbId }, chat] = await Promise.all([
authApp({
req,
authToken: true,
appId,
per: 'r'
}),
chatId
? authChat({
req,
authToken: true,
chatId,
per: 'r'
})
: undefined
chatId ? MongoChat.findOne({ chatId }) : undefined
]);
// get app and history
const { history = [] }: { history?: ChatItemType[] } = await (async () => {
if (chatId) {
// auth chatId
const history = await MongoChatItem.find(
{
chatId
},
`dataId obj value adminFeedback userFeedback ${ModuleOutputKeyEnum.responseData}`
)
.sort({ _id: -1 })
.limit(30);
history.reverse();
return { history };
// auth chat permission
if (!app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
throw new Error(ChatErrEnum.unAuthChat);
}
return {};
})();
// get app and history
const { history } = await getChatItems({
chatId,
limit: 30,
field: `dataId obj value adminFeedback userFeedback ${ModuleOutputKeyEnum.responseData}`
});
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId,
title: chat?.title || '新对话',
userAvatar: undefined,
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
name: app.name,
avatar: app.avatar,
intro: app.intro
},
title: autChatResult?.chat?.title || '新对话',
variables: autChatResult?.chat?.variables || {},
history
}
}
});
} 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 { connectToDatabase } from '@/service/mongo';
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) {
try {
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({
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 { jsonRes } from '@fastgpt/service/common/response';
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 { 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) {
try {
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(
{ 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>) {
try {
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;
if (!id) {
@@ -26,11 +26,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
...(parentId !== undefined && { parentId: parentId || null }),
...(name && { name }),
...(avatar && { avatar }),
...(tags && { tags }),
...(permission && { permission }),
...(agentModel && { agentModel: agentModel.model }),
...(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 { createJWT, setCookie } from '@fastgpt/service/support/permission/controller';
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';
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 { authCert } from '@fastgpt/service/support/permission/auth/common';
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) {
try {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
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 { getUploadModel } from '@fastgpt/service/common/file/upload/multer';
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 { ChatMessageItemType } from '@fastgpt/global/core/ai/type.d';
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 { responseWrite } from '@fastgpt/service/common/response';
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 requestIp from 'request-ip';
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 { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
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 { MongoApp } from '@fastgpt/service/core/app/schema';
import { autChatCrud } from '@/service/support/permission/auth/chat';
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
@@ -32,7 +33,7 @@ type FastGptWebChatProps = {
};
type FastGptShareChatProps = {
shareId?: string;
authToken?: string;
outLinkUid?: string;
};
export type Props = ChatCompletionCreateParams &
FastGptWebChatProps &
@@ -56,11 +57,11 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
res.end();
});
let {
const {
chatId,
appId,
shareId,
authToken,
outLinkUid,
stream = false,
detail = false,
messages = [],
@@ -94,21 +95,28 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
/* auth app permission */
const { user, app, responseDetail, authType, apikey, canWrite } = await (async () => {
if (shareId) {
const { user, app, authType, responseDetail } = await authOutLinkChat({
const { user, app, responseDetail, authType, apikey, canWrite, uid } = await (async () => {
if (shareId && outLinkUid) {
const { user, appId, authType, responseDetail, uid } = await authOutLinkChatStart({
shareId,
ip: requestIp.getClientIp(req),
authToken,
outLinkUid,
question: question.value
});
const app = await MongoApp.findById(appId);
if (!app) {
return Promise.reject('app is empty');
}
return {
user,
app,
responseDetail,
apikey: '',
authType,
canWrite: false
canWrite: false,
uid
};
}
@@ -146,11 +154,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
};
}
// token auth
if (!appId) {
return Promise.reject('appId is empty');
}
// token
const { app, canWrite } = await authApp({
req,
authToken: true,
@@ -168,12 +175,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
};
})();
// auth app, get history
const { history } = await getChatHistory({ chatId, tmbId: user.team.tmbId });
// auth chat permission
await autChatCrud({
req,
authToken: true,
authApiKey: true,
chatId,
shareId,
outLinkUid,
per: 'w'
});
const isAppOwner = !shareId && String(user.team.tmbId) === String(app.tmbId);
/* format prompts */
// get and concat history
const { history } = await getChatItems({ chatId, limit: 30, field: `dataId obj value` });
const concatHistory = history.concat(chatMessages);
/* start flow controller */
@@ -202,8 +216,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
teamId: user.team.teamId,
tmbId: user.team.tmbId,
variables,
updateUseTime: isAppOwner, // owner update use time
updateUseTime: !shareId && String(user.team.tmbId) === String(app.tmbId), // owner update use time
shareId,
outLinkUid: uid,
source: (() => {
if (shareId) {
return ChatSourceEnum.share;
@@ -281,7 +296,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
if (shareId) {
pushResult2Remote({ authToken, shareId, responseData });
pushResult2Remote({ outLinkUid, shareId, responseData });
updateOutLinkUsage({
shareId,
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 ChatBox, { type ComponentRef } from '@/components/ChatBox';
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 MyModal from '@/components/MyModal';
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
@@ -199,7 +199,7 @@ function DetailLogsModal({
const { data: chat } = useQuery(
['getChatDetail', chatId],
() => getInitChatSiteInfo({ appId, chatId }),
() => getInitChatInfo({ appId, chatId }),
{
onSuccess(res) {
const history = res.history.map((item) => ({

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useRef } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { getInitChatSiteInfo, delChatRecordById, putChatHistory } from '@/web/core/chat/api';
import { getInitChatInfo, putChatHistory } from '@/web/core/chat/api';
import {
Box,
Flex,
@@ -34,6 +34,7 @@ import { serviceSideProps } from '@/web/common/utils/i18n';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { checkChatSupportSelectFileByChatModels } from '@/web/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 router = useRouter();
@@ -49,13 +50,15 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
setLastChatAppId,
lastChatId,
setLastChatId,
history,
loadHistory,
histories,
loadHistories,
pushHistory,
updateHistory,
delHistory,
clearHistory,
delOneHistory,
clearHistories,
chatData,
setChatData
setChatData,
delOneHistoryItem
} = useChatStore();
const { myApps, loadMyApps } = useAppStore();
const { userInfo } = useUserStore();
@@ -85,7 +88,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
prompts[1]?.value?.slice(0, 20) ||
'新对话';
// update history
// new chat
if (completionChatId !== chatId) {
const newHistory: ChatHistoryItemType = {
chatId: completionChatId,
@@ -94,7 +97,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
appId,
top: false
};
updateHistory(newHistory);
pushHistory(newHistory);
if (controller.signal.reason !== 'leave') {
forbidRefresh.current = true;
router.replace({
@@ -105,7 +108,8 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
});
}
} else {
const currentChat = history.find((item) => item.chatId === chatId);
// update chat
const currentChat = histories.find((item) => item.chatId === chatId);
currentChat &&
updateHistory({
...currentChat,
@@ -117,30 +121,12 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
setChatData((state) => ({
...state,
title: newTitle,
history: ChatBoxRef.current?.getChatHistory() || state.history
history: ChatBoxRef.current?.getChatHistories() || state.history
}));
return { responseText, responseData, isNewChat: forbidRefresh.current };
},
[appId, chatId, history, 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]
[appId, chatId, histories, pushHistory, router, setChatData, updateHistory]
);
// get chat app info
@@ -156,10 +142,10 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
}) => {
try {
loading && setIsLoading(true);
const res = await getInitChatSiteInfo({ appId, chatId });
const res = await getInitChatInfo({ appId, chatId });
const history = res.history.map((item) => ({
...item,
status: 'finish' as any
status: ChatStatusEnum.finish
}));
setChatData({
@@ -185,8 +171,13 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
});
if (e?.code === 501) {
router.replace('/app/list');
} else {
router.replace('/chat');
} else if (chatId) {
router.replace({
query: {
...router.query,
chatId: ''
}
});
}
}
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 (
<Flex h={'100%'}>
@@ -289,7 +280,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
appAvatar={chatData.app.avatar}
activeChatId={chatId}
onClose={onCloseSlider}
history={history.map((item, i) => ({
history={histories.map((item, i) => ({
id: item.chatId,
title: item.title,
customTitle: item.customTitle,
@@ -306,39 +297,24 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
onCloseSlider();
}
}}
onDelHistory={delHistory}
onDelHistory={delOneHistory}
onClearHistory={() => {
clearHistory(appId);
clearHistories({ appId });
router.replace({
query: {
appId
}
});
}}
onSetHistoryTop={async (e) => {
try {
await putChatHistory(e);
const historyItem = history.find((item) => item.chatId === e.chatId);
if (!historyItem) return;
updateHistory({
...historyItem,
top: e.top
});
} catch (error) {}
onSetHistoryTop={(e) => {
updateHistory(e);
}}
onSetCustomTitle={async (e) => {
try {
putChatHistory({
chatId: e.chatId,
customTitle: e.title
});
const historyItem = history.find((item) => item.chatId === e.chatId);
if (!historyItem) return;
updateHistory({
...historyItem,
chatId: e.chatId,
title: e.title,
customTitle: e.title
});
} catch (error) {}
}}
/>
)}
@@ -372,7 +348,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
feedbackType={'user'}
onUpdateVariable={(e) => {}}
onStartChat={startChat}
onDelMessage={delOneHistoryItem}
onDelMessage={(e) => delOneHistoryItem({ ...e, chatId })}
/>
</Box>
</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 { useRouter } from 'next/router';
import { initShareChatInfo } from '@/web/support/outLink/api';
import { Box, Flex, useDisclosure, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react';
import { useToast } from '@/web/common/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useQuery } from '@tanstack/react-query';
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 { gptMessage2ChatType } from '@/utils/adapt';
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';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
@@ -21,6 +20,13 @@ import ChatHeader from './components/ChatHeader';
import ChatHistorySlider from './components/ChatHistorySlider';
import { serviceSideProps } from '@/web/common/utils/i18n';
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 = ({
shareId,
@@ -33,6 +39,7 @@ const OutLink = ({
showHistory: '0' | '1';
authToken?: string;
}) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
@@ -43,18 +50,23 @@ const OutLink = ({
const ChatBoxRef = useRef<ComponentRef>(null);
const {
shareChatData,
setShareChatData,
shareChatHistory,
saveChatResponse,
delShareChatHistoryItemById,
delOneShareHistoryByChatId,
delManyShareChatHistoryByShareId
localUId,
shareChatHistory, // abandon
clearLocalHistory // abandon
} = useShareChatStore();
const history = useMemo(
() => shareChatHistory.filter((item) => item.shareId === shareId),
[shareChatHistory, shareId]
);
const {
histories,
loadHistories,
pushHistory,
updateHistory,
delOneHistory,
chatData,
setChatData,
delOneHistoryItem,
clearHistories
} = useChatStore();
const appId = chatData.appId;
const outLinkUid: string = authToken || localUId;
const startChat = useCallback(
async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
@@ -67,28 +79,28 @@ const OutLink = ({
variables,
shareId,
chatId: completionChatId,
authToken
outLinkUid
},
onMessage: generatingMessage,
abortSignal: controller
});
const result: ChatSiteItemType[] = gptMessage2ChatType(prompts).map((item) => ({
...item,
status: 'finish'
}));
result[1].value = responseText;
result[1].responseData = responseData;
const newTitle =
chatContentReplaceBlock(prompts[0].content).slice(0, 20) ||
prompts[1]?.value?.slice(0, 20) ||
'新对话';
/* save chat */
saveChatResponse({
// new chat
if (completionChatId !== chatId) {
const newHistory: ChatHistoryItemType = {
chatId: completionChatId,
prompts: result,
variables,
shareId
});
if (completionChatId !== chatId && controller.signal.reason !== 'leave') {
updateTime: new Date(),
title: newTitle,
appId,
top: false
};
pushHistory(newHistory);
if (controller.signal.reason !== 'leave') {
forbidRefresh.current = true;
router.replace({
query: {
@@ -97,6 +109,31 @@ const OutLink = ({
}
});
}
} 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) => ({
...item,
status: 'finish'
}));
result[1].value = responseText;
result[1].responseData = responseData;
window.top?.postMessage(
{
@@ -111,61 +148,80 @@ const OutLink = ({
return { responseText, responseData, isNewChat: forbidRefresh.current };
},
[authToken, chatId, router, saveChatResponse, shareId]
[chatId, shareId, outLinkUid, setChatData, appId, updateHistory, router, histories]
);
const loadAppInfo = useCallback(
async (shareId: string, chatId: string, authToken?: string) => {
const loadChatInfo = useCallback(
async (shareId: string, chatId: string) => {
if (!shareId) return null;
const history = shareChatHistory.find((item) => item.chatId === chatId) || defaultHistory;
ChatBoxRef.current?.resetHistory(history.chats);
ChatBoxRef.current?.resetVariables(history.variables);
try {
const chatData = await (async () => {
if (shareChatData.app.name === '') {
return initShareChatInfo({
const res = await getInitOutLinkChatInfo({
chatId,
shareId,
authToken
outLinkUid: authToken || localUId
});
}
return shareChatData;
})();
const history = res.history.map((item) => ({
...item,
status: ChatStatusEnum.finish
}));
setShareChatData({
...chatData,
setChatData({
...res,
history
});
} catch (e: any) {
toast({
status: 'error',
title: getErrText(e, '获取应用失败')
});
if (e?.code === 501) {
delManyShareChatHistoryByShareId(shareId);
}
}
if (history.chats.length > 0) {
ChatBoxRef.current?.resetHistory(history);
ChatBoxRef.current?.resetVariables(res.variables);
if (res.history.length > 0) {
setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto');
}, 500);
}
} catch (e: any) {
toast({
status: 'error',
title: getErrText(e, t('core.shareChat.Init Error'))
});
if (chatId) {
router.replace({
query: {
...router.query,
chatId: ''
}
});
}
if (e?.statusText === OutLinkErrEnum.linkUnInvalid) {
router.replace('/');
}
}
return history;
return null;
},
[delManyShareChatHistoryByShareId, setShareChatData, shareChatData, shareChatHistory, toast]
[authToken, localUId, router, setChatData, t, toast]
);
useQuery(['init', shareId, chatId, authToken], () => {
useQuery(['init', shareId, chatId], () => {
if (forbidRefresh.current) {
forbidRefresh.current = false;
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(() => {
if (window !== top) {
window.top?.postMessage({ type: 'shareChatReady' }, '*');
@@ -173,10 +229,32 @@ const OutLink = ({
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 (
<PageContainer {...(isEmbed ? { p: '0 !important', borderRadius: '0' } : {})}>
<Head>
<title>{shareChatData.app.name}</title>
<title>{chatData.app.name}</title>
</Head>
<Flex h={'100%'} flexDirection={['column', 'row']}>
{showHistory === '1'
@@ -199,12 +277,14 @@ const OutLink = ({
);
})(
<ChatHistorySlider
appName={shareChatData.app.name}
appAvatar={shareChatData.app.avatar}
appName={chatData.app.name}
appAvatar={chatData.app.avatar}
activeChatId={chatId}
history={history.map((item) => ({
history={histories.map((item) => ({
id: item.chatId,
title: item.title
title: item.title,
customTitle: item.customTitle,
top: item.top
}))}
onClose={onCloseSlider}
onChangeChat={(chatId) => {
@@ -218,9 +298,9 @@ const OutLink = ({
onCloseSlider();
}
}}
onDelHistory={delOneShareHistoryByChatId}
onDelHistory={({ chatId }) => delOneHistory({ chatId, shareId, outLinkUid })}
onClearHistory={() => {
delManyShareChatHistoryByShareId(shareId);
clearHistories({ shareId, outLinkUid });
router.replace({
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}
@@ -242,36 +332,24 @@ const OutLink = ({
>
{/* header */}
<ChatHeader
appAvatar={shareChatData.app.avatar}
appName={shareChatData.app.name}
history={shareChatData.history.chats}
appAvatar={chatData.app.avatar}
appName={chatData.app.name}
history={chatData.history}
onOpenSlider={onOpenSlider}
/>
{/* chat box */}
<Box flex={1}>
<ChatBox
active={!!shareChatData.app.name}
active={!!chatData.app.name}
ref={ChatBoxRef}
appAvatar={shareChatData.app.avatar}
userAvatar={shareChatData.userAvatar}
userGuideModule={shareChatData.app?.userGuideModule}
showFileSelector={checkChatSupportSelectFileByChatModels(
shareChatData.app.chatModels
)}
appAvatar={chatData.app.avatar}
userAvatar={chatData.userAvatar}
userGuideModule={chatData.app?.userGuideModule}
showFileSelector={checkChatSupportSelectFileByChatModels(chatData.app.chatModels)}
feedbackType={'user'}
onUpdateVariable={(e) => {
setShareChatData((state) => ({
...state,
history: {
...state.history,
variables: e
}
}));
}}
onUpdateVariable={(e) => {}}
onStartChat={startChat}
onDelMessage={({ contentId, index }) =>
delShareChatHistoryItemById({ chatId, contentId, index })
}
onDelMessage={(e) => delOneHistoryItem({ ...e, chatId })}
/>
</Box>
</Flex>

View File

@@ -189,15 +189,6 @@ export async function searchDatasetData(props: SearchProps) {
})
).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 {
searchRes: reRankResults.slice(0, limit),
tokenLen
@@ -382,7 +373,7 @@ export async function reRankSearchResult({
} catch (error) {
console.log(error);
return [];
return data;
}
}
// ------------------ 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 { RunningModuleItemType } from '@/types/app';
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 { ModuleItemType } from '@fastgpt/global/core/module/type';
import { UserType } from '@fastgpt/global/support/user/type';

View File

@@ -48,7 +48,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
answerText,
responseData: {
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),
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 { AuthLinkChatProps } from '@fastgpt/global/support/outLink/api.d';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { getUserAndAuthBalance } from './user';
import { POST } from '@fastgpt/service/common/api/plusRequest';
import type {
AuthOutLinkChatProps,
AuthOutLinkLimitProps,
AuthOutLinkInitProps,
AuthOutLinkResponse
} from '@fastgpt/global/support/outLink/api.d';
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,
ip,
authToken,
outLinkUid,
question
}: AuthLinkChatProps & {
}: AuthOutLinkChatProps & {
shareId: string;
}) {
// get outLink
const { shareChat, app } = await authOutLinkValid({ shareId });
// get outLink and app
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 }),
global.feConfigs?.isPlus
? authOutLinkLimit({ outLink: shareChat, ip, authToken, question })
: undefined
authOutLinkChatLimit({ outLink: shareChat, ip, outLinkUid, question })
]);
return {
authType: AuthUserTypeEnum.token,
responseDetail: shareChat.responseDetail,
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 { 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 { addLog } from '@fastgpt/service/common/mongo/controller';
import type { ConcatBillProps, CreateBillProps } from '@fastgpt/global/support/wallet/bill/api.d';
@@ -41,7 +41,7 @@ export const pushChatBill = ({
source: `${BillSourceEnum}`;
response: ChatHistoryItemResType[];
}) => {
const total = response.reduce((sum, item) => sum + item.price, 0);
const total = response.reduce((sum, item) => sum + (item.price || 0), 0);
createBill({
teamId,

View File

@@ -15,6 +15,7 @@ type Props = {
updateUseTime: boolean;
source: `${ChatSourceEnum}`;
shareId?: string;
outLinkUid?: string;
content: [ChatItemType, ChatItemType];
};
@@ -27,10 +28,11 @@ export async function saveChat({
updateUseTime,
source,
shareId,
outLinkUid,
content
}: Props) {
try {
const chatHistory = await MongoChat.findOne(
const chat = await MongoChat.findOne(
{
chatId,
teamId,
@@ -57,7 +59,7 @@ export async function saveChat({
content[1]?.value?.slice(0, 20) ||
'Chat';
if (chatHistory) {
if (chat) {
promise.push(
MongoChat.updateOne(
{ chatId },
@@ -77,7 +79,8 @@ export async function saveChat({
variables,
title,
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 = [
'moduleType',
'moduleName',
'moduleLogo',
'runningTime',
'historyPreview',
'quoteList',
'question'
];
@@ -17,6 +22,6 @@ export function selectShareResponse({ responseData }: { responseData: ChatHistor
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 { getErrText } from '@fastgpt/global/common/error/utils';
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 { getToken } from '@/web/support/user/auth';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';

View File

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

View File

@@ -1,42 +1,53 @@
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
import type { InitChatResponse } from '@fastgpt/global/core/chat/api.d';
import type { RequestPaging } from '@/types';
import { UpdateHistoryProps } from '@fastgpt/global/core/chat/api.d';
import type { AdminUpdateFeedbackParams } from '@fastgpt/global/core/chat/api.d';
import { GetChatSpeechProps } from '@/global/core/chat/api.d';
import type {
InitChatProps,
InitChatResponse,
InitOutLinkChatProps,
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);
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 }) =>
POST<ChatHistoryItemType[]>('/core/chat/list', data);
export const getChatHistories = (data: getHistoriesProps) =>
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
*/
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);
/**
* 修改历史记录: 标题/置顶
*/
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 }) =>
POST('/core/chat/feedback/userUpdate', data);

View File

@@ -2,35 +2,36 @@ import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
import type { InitChatResponse } from '@fastgpt/global/core/chat/api';
import { delChatHistoryById, getChatHistory, clearChatHistoryByAppId } from '@/web/core/chat/api';
import type {
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 = {
history: ChatHistoryItemType[];
loadHistory: (data: { appId: string }) => Promise<null>;
delHistory(history: string): Promise<void>;
clearHistory(appId: string): Promise<void>;
updateHistory: (history: ChatHistoryItemType) => void;
histories: ChatHistoryItemType[];
loadHistories: (data: getHistoriesProps) => Promise<null>;
delOneHistory(data: DelHistoryProps): Promise<void>;
clearHistories(data: ClearHistoriesProps): Promise<void>;
pushHistory: (history: ChatHistoryItemType) => void;
updateHistory: (e: UpdateHistoryProps & { updateTime?: Date; title?: string }) => Promise<any>;
chatData: InitChatResponse;
setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void;
lastChatAppId: string;
setLastChatAppId: (id: string) => void;
lastChatId: string;
setLastChatId: (id: string) => void;
};
const defaultChatData: InitChatResponse = {
chatId: '',
appId: '',
app: {
name: 'Loading',
avatar: '/icon/logo.svg',
intro: '',
canUse: false
},
title: '新对话',
variables: {},
history: []
delOneHistoryItem: (e: { chatId: string; contentId?: string; index: number }) => Promise<any>;
};
export const useChatStore = create<State>()(
@@ -49,49 +50,62 @@ export const useChatStore = create<State>()(
state.lastChatId = id;
});
},
history: [],
async loadHistory({ appId }) {
const oneHistory = get().history[0];
if (oneHistory && oneHistory.appId === appId) return null;
const data = await getChatHistory({
appId,
pageNum: 1,
pageSize: 20
});
histories: [],
async loadHistories(e) {
const data = await getChatHistories(e);
set((state) => {
state.history = data;
state.histories = data;
});
return null;
},
async delHistory(chatId) {
async delOneHistory(props) {
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) => {
state.history = [];
state.histories = [];
});
await clearChatHistoryByAppId(appId);
await clearChatHistoryByAppId(data);
},
updateHistory(history) {
const index = get().history.findIndex((item) => item.chatId === history.chatId);
pushHistory(history) {
set((state) => {
const newHistory = (() => {
state.histories = [history, ...state.histories];
});
},
async updateHistory(props) {
const { chatId, customTitle, top, title, updateTime } = props;
const index = get().histories.findIndex((item) => item.chatId === chatId);
if (index > -1) {
return [
history,
...get().history.slice(0, index),
...get().history.slice(index + 1)
];
} else {
return [history, ...state.history];
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.history = newHistory;
state.histories = newHistories;
});
}
},
chatData: defaultChatData,
setChatData(e = defaultChatData) {
@@ -104,6 +118,19 @@ export const useChatStore = create<State>()(
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 { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type {
ShareChatHistoryItemType,
ShareChatType
} from '@fastgpt/global/support/outLink/api.d';
import type { ChatSiteItemType } from '@fastgpt/global/core/chat/type.d';
import { HUMAN_ICON } from '@fastgpt/global/core/chat/constants';
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWSYZ1234567890_',
24
);
type State = {
shareChatData: ShareChatType;
setShareChatData: (e: ShareChatType | ((e: ShareChatType) => ShareChatType)) => void;
shareChatHistory: ShareChatHistoryItemType[];
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
localUId: string;
shareChatHistory: (ChatHistoryItemType & { delete?: boolean })[];
clearLocalHistory: (shareId?: string) => void;
};
export const useShareChatStore = create<State>()(
devtools(
persist(
immer((set, get) => ({
shareChatData: defaultShareChatData,
setShareChatData(e) {
const val = (() => {
if (typeof e === 'function') {
return e(get().shareChatData);
} else {
return e;
}
})();
localUId: `shareChat-${Date.now()}-${nanoid()}`,
shareChatHistory: [], // old version field
clearLocalHistory() {
// abandon
set((state) => {
state.shareChatData = val;
// update history
state.shareChatHistory = state.shareChatHistory.map((item) =>
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
? {
state.shareChatHistory = state.shareChatHistory.map((item) => ({
...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 = [];
}
delete: true
}));
});
}
})),
{
name: 'shareChatStore',
partialize: (state) => ({
localUId: state.localUId,
shareChatHistory: state.shareChatHistory
})
}

View File

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

View File

@@ -1,13 +1,6 @@
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';
/**
* 初始化分享聊天
*/
export const initShareChatInfo = (data: { shareId: string; authToken?: string }) =>
GET<InitShareChatResponse>(`/support/outLink/init`, data);
/**
* create a shareChat
*/

View File

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