Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c223d3a2c6 | ||
|
|
ae4c479f37 | ||
|
|
6a996272da | ||
|
|
1bf76ebe7a | ||
|
|
a19afca148 | ||
|
|
be3b680bc6 | ||
|
|
31dbcfde9f | ||
|
|
6d438aafdf | ||
|
|
1aaafcf631 | ||
|
|
7521bce77e | ||
|
|
c8dee29dc4 | ||
|
|
8f953d1fc4 | ||
|
|
970b62be25 | ||
|
|
b2b3aa651d | ||
|
|
b0e7d25464 | ||
|
|
b46048609c | ||
|
|
ae2887e956 | ||
|
|
7917766024 | ||
|
|
a1a63260dd |
@@ -18,6 +18,8 @@ FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开
|
||||
<a href="https://github.com/labring/FastGPT#-%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE">相关项目</a>
|
||||
</p>
|
||||
|
||||
https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409bd33f6d4
|
||||
|
||||
## 🛸 在线体验
|
||||
|
||||
[fastgpt.run](https://fastgpt.run/)(服务器在新加坡,部分地区可能无法直连)
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"show_doc": true,
|
||||
"systemTitle": "FastGPT",
|
||||
"authorText": "Made by FastGPT Team.",
|
||||
"exportLimitMinutes": 0,
|
||||
"limit": {
|
||||
"exportLimitMinutes": 0
|
||||
},
|
||||
"scripts": []
|
||||
},
|
||||
"SystemParams": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "fastgpt",
|
||||
"version": "3.7",
|
||||
"private": true,
|
||||
"version": "4.2.1",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
### Fast GPT V4.4
|
||||
### Fast GPT V4.4.1
|
||||
|
||||
1. 新增 - 知识库目录结构
|
||||
2. 优化 - [使用文档](https://doc.fastgpt.run/docs/intro/)
|
||||
3. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
|
||||
4. [点击查看商业版](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
|
||||
2. 新增 - 分享链接支持配置 IP 限流、过期时间、最大额度等
|
||||
3. 优化 - [使用文档](https://doc.fastgpt.run/docs/intro/)
|
||||
4. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
|
||||
5. [点击查看商业版](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
"Confirm": "Yes",
|
||||
"Create New": "Create",
|
||||
"Dataset": "Dataset",
|
||||
"Export": "Export",
|
||||
"Folder": "Folder",
|
||||
"Move": "Move",
|
||||
"Name": "Name",
|
||||
"Rename": "Rename",
|
||||
"Running": "Running",
|
||||
"Select value is empty": "Select value is empty",
|
||||
"UnKnow": "UnKnow",
|
||||
@@ -81,14 +84,18 @@
|
||||
"Delete Failed": "Delete Failed",
|
||||
"Delete Success": "Delete Successful",
|
||||
"Delete Warning": "Warning",
|
||||
"Edit": "Edit",
|
||||
"Expired Time": "Expired",
|
||||
"Filed is repeat": "Filed is repeated",
|
||||
"Filed is repeated": "",
|
||||
"Input": "Input",
|
||||
"Name is empty": "Name is empty",
|
||||
"Output": "Output",
|
||||
"Password inconsistency": "Password inconsistency",
|
||||
"Rename": "Rename",
|
||||
"Search": "Search",
|
||||
"Status": "Status",
|
||||
"Update Successful": "Update Successful",
|
||||
"export": ""
|
||||
},
|
||||
"dataset": {
|
||||
@@ -163,6 +170,7 @@
|
||||
},
|
||||
"kb": {
|
||||
"Chunk Length": "Chunk Length",
|
||||
"Confirm move the folder": "Confirm Move",
|
||||
"Confirm to delete the file": "Are you sure to delete the file and all its data?",
|
||||
"Create Folder": "Create Folder",
|
||||
"Delete Dataset Error": "Delete dataset failed",
|
||||
@@ -171,7 +179,9 @@
|
||||
"Filename": "Filename",
|
||||
"Files": "{{total}} Files",
|
||||
"Folder Name": "Input folder name",
|
||||
"Move Failed": "Move Failed",
|
||||
"My Dataset": "My Dataset",
|
||||
"No Folder": "No Folder",
|
||||
"Other Data": "Other Data",
|
||||
"Select Dataset": "Select Dataset",
|
||||
"Select Folder": "Enter folder",
|
||||
@@ -187,6 +197,28 @@
|
||||
"Store": "Store",
|
||||
"Tools": "Tools"
|
||||
},
|
||||
"outlink": {
|
||||
"Copy Iframe": "Copy Iframe",
|
||||
"Copy Link": "Copy",
|
||||
"Create Ifrme Window": "Create Iframe Link",
|
||||
"Create Share Window": "Create Share Window",
|
||||
"Delete Link": "Delete",
|
||||
"Edit Ifrme Link": "Edit Iframe Link",
|
||||
"Edit Link": "Edit",
|
||||
"Edit Share Window": "Edit Share Window",
|
||||
"Link Name": "Link Name",
|
||||
"Link is empty": "",
|
||||
"Max credit": "Credit",
|
||||
"Max credit tips": "What is the maximum amount of money that can be consumed by the link? If the link is exceeded, it will be banned. -1 indicates no limit.",
|
||||
"QPM": "QPM",
|
||||
"QPM Tips": "The maximum number of queries per IP address per minute",
|
||||
"QPM is empty": "QPM is empty",
|
||||
"Response Detail": "Detail",
|
||||
"Response Detail tips": "Whether detailed data such as references and full context need to be returned"
|
||||
},
|
||||
"system": {
|
||||
"Help Document": "Document"
|
||||
},
|
||||
"user": {
|
||||
"Account": "Account",
|
||||
"Amount of earnings": "Earnings",
|
||||
@@ -199,6 +231,7 @@
|
||||
"Copy invite url": "Copy invitation link",
|
||||
"Invite Url": "Invite Url",
|
||||
"Invite url tip": "Friends who register through this link will be permanently bound to you, and you will get a certain balance reward when they recharge. In addition, when friends register with their mobile phone number, you will get 5 yuan reward immediately.",
|
||||
"Language": "Language",
|
||||
"Notice": "Notice",
|
||||
"Old password is error": "Old password is error",
|
||||
"OpenAI Account Setting": "OpenAI Account Setting",
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
"Confirm": "确认",
|
||||
"Create New": "新建",
|
||||
"Dataset": "知识库",
|
||||
"Export": "导出",
|
||||
"Folder": "文件夹",
|
||||
"Move": "移动",
|
||||
"Name": "名称",
|
||||
"Rename": "重命名",
|
||||
"Running": "运行中",
|
||||
"Select value is empty": "选择的内容为空",
|
||||
"UnKnow": "未知",
|
||||
@@ -81,14 +84,18 @@
|
||||
"Delete Failed": "删除失败",
|
||||
"Delete Success": "删除成功",
|
||||
"Delete Warning": "删除警告",
|
||||
"Edit": "编辑",
|
||||
"Expired Time": "过期时间",
|
||||
"Filed is repeat": "",
|
||||
"Filed is repeated": "字段重复了",
|
||||
"Input": "输入",
|
||||
"Name is empty": "名称不能为空",
|
||||
"Output": "输出",
|
||||
"Password inconsistency": "两次密码不一致",
|
||||
"Rename": "重命名",
|
||||
"Search": "搜索",
|
||||
"Status": "状态",
|
||||
"Update Successful": "更新成功",
|
||||
"export": ""
|
||||
},
|
||||
"dataset": {
|
||||
@@ -163,6 +170,7 @@
|
||||
},
|
||||
"kb": {
|
||||
"Chunk Length": "数据总量",
|
||||
"Confirm move the folder": "确认移动到该目录",
|
||||
"Confirm to delete the file": "确认删除该文件及其所有数据?",
|
||||
"Create Folder": "创建文件夹",
|
||||
"Delete Dataset Error": "删除知识库异常",
|
||||
@@ -171,7 +179,9 @@
|
||||
"Filename": "文件名",
|
||||
"Files": "文件: {{total}}个",
|
||||
"Folder Name": "输入文件夹名称",
|
||||
"Move Failed": "移动出现错误~",
|
||||
"My Dataset": "我的知识库",
|
||||
"No Folder": "没有子目录了~",
|
||||
"Other Data": "其他数据",
|
||||
"Select Dataset": "选择该知识库",
|
||||
"Select Folder": "进入文件夹",
|
||||
@@ -187,6 +197,28 @@
|
||||
"Store": "应用市场",
|
||||
"Tools": "工具"
|
||||
},
|
||||
"outlink": {
|
||||
"Copy Iframe": "复制嵌入",
|
||||
"Copy Link": "复制",
|
||||
"Create Ifrme Window": "创建嵌入链接",
|
||||
"Create Share Window": "创建免登录窗口",
|
||||
"Delete Link": "删除链接",
|
||||
"Edit Ifrme Link": "更新嵌入链接",
|
||||
"Edit Link": "编辑",
|
||||
"Edit Share Window": "更新分享窗口",
|
||||
"Link Name": "分享链接的名字",
|
||||
"Link is empty": "",
|
||||
"Max credit": "最大金额",
|
||||
"Max credit tips": "该链接最大可消耗多少金额,超出后链接将被禁止使用。-1 代表无限制。",
|
||||
"QPM": "",
|
||||
"QPM Tips": "每个 IP 每分钟最多提问多少次",
|
||||
"QPM is empty": "QPM 不能为空",
|
||||
"Response Detail": "返回详情",
|
||||
"Response Detail tips": "是否需要返回引用、完整上下文等详细数据"
|
||||
},
|
||||
"system": {
|
||||
"Help Document": "帮助文档"
|
||||
},
|
||||
"user": {
|
||||
"Account": "账号",
|
||||
"Amount of earnings": "收益(¥)",
|
||||
@@ -199,6 +231,7 @@
|
||||
"Copy invite url": "复制邀请链接",
|
||||
"Invite Url": "邀请链接",
|
||||
"Invite url tip": "通过该链接注册的好友将永久与你绑定,其充值时你会获得一定余额奖励。\n此外,好友使用手机号注册时,你将立即获得 5 元奖励。",
|
||||
"Language": "语言",
|
||||
"Notice": "通知",
|
||||
"Old password is error": "旧密码错误",
|
||||
"OpenAI Account Setting": "OpenAI 账号配置",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { GET, POST, DELETE, PUT } from './request';
|
||||
import type { ChatHistoryItemType } from '@/types/chat';
|
||||
import type { InitChatResponse, InitShareChatResponse } from './response/chat';
|
||||
import type { InitChatResponse } from './response/chat';
|
||||
import { RequestPaging } from '../types/index';
|
||||
import type { OutLinkSchema } from '@/types/mongoSchema';
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory';
|
||||
import { AdminUpdateFeedbackParams } from './request/chat';
|
||||
|
||||
@@ -40,32 +38,6 @@ export const delChatRecordById = (data: { chatId: string; contentId: string }) =
|
||||
export const putChatHistory = (data: UpdateHistoryProps) =>
|
||||
PUT('/chat/history/updateChatHistory', data);
|
||||
|
||||
/**
|
||||
* 初始化分享聊天
|
||||
*/
|
||||
export const initShareChatInfo = (data: { shareId: string }) =>
|
||||
GET<InitShareChatResponse>(`/chat/shareChat/init`, data);
|
||||
|
||||
/**
|
||||
* create a shareChat
|
||||
*/
|
||||
export const createShareChat = (
|
||||
data: ShareChatEditType & {
|
||||
appId: string;
|
||||
}
|
||||
) => POST<string>(`/chat/shareChat/create`, data);
|
||||
|
||||
/**
|
||||
* get shareChat
|
||||
*/
|
||||
export const getShareChatList = (appId: string) =>
|
||||
GET<OutLinkSchema[]>(`/chat/shareChat/list`, { appId });
|
||||
|
||||
/**
|
||||
* delete a shareChat
|
||||
*/
|
||||
export const delShareChatById = (id: string) => DELETE(`/chat/shareChat/delete?id=${id}`);
|
||||
|
||||
export const userUpdateChatFeedback = (data: { chatItemId: string; userFeedback?: string }) =>
|
||||
POST('/chat/feedback/userUpdate', data);
|
||||
|
||||
|
||||
8
client/src/api/core/dataset/file.d.ts
vendored
Normal file
8
client/src/api/core/dataset/file.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { RequestPaging } from '../../../types/index';
|
||||
|
||||
export type GetFileListProps = RequestPaging & {
|
||||
kbId: string;
|
||||
searchText: string;
|
||||
};
|
||||
|
||||
export type UpdateFileProps = { id: string; name?: string; datasetUsed?: boolean };
|
||||
15
client/src/api/core/dataset/file.ts
Normal file
15
client/src/api/core/dataset/file.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { GET, POST, PUT, DELETE } from '@/api/request';
|
||||
import type { FileInfo, KbFileItemType } from '@/types/plugin';
|
||||
|
||||
import type { GetFileListProps, UpdateFileProps } from './file.d';
|
||||
|
||||
export const getDatasetFiles = (data: GetFileListProps) =>
|
||||
POST<KbFileItemType[]>(`/core/dataset/file/list`, data);
|
||||
export const delDatasetFileById = (params: { fileId: string; kbId: string }) =>
|
||||
DELETE(`/core/dataset/file/delById`, params);
|
||||
export const getFileInfoById = (fileId: string) =>
|
||||
GET<FileInfo>(`/core/dataset/file/detail`, { fileId });
|
||||
export const delDatasetEmptyFiles = (kbId: string) =>
|
||||
DELETE(`/core/dataset/file/delEmptyFiles`, { kbId });
|
||||
|
||||
export const updateDatasetFile = (data: UpdateFileProps) => PUT(`/core/dataset/file/update`, data);
|
||||
@@ -1,12 +1,5 @@
|
||||
import { GET, POST, PUT, DELETE } from '../request';
|
||||
import type {
|
||||
DatasetItemType,
|
||||
FileInfo,
|
||||
KbFileItemType,
|
||||
KbItemType,
|
||||
KbListItemType,
|
||||
KbPathItemType
|
||||
} from '@/types/plugin';
|
||||
import type { DatasetItemType, KbItemType, KbListItemType, KbPathItemType } from '@/types/plugin';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import {
|
||||
Props as PushDataProps,
|
||||
@@ -19,10 +12,11 @@ import {
|
||||
import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData';
|
||||
import type { KbUpdateParams, CreateKbParams, GetKbDataListProps } from '../request/kb';
|
||||
import { QuoteItemType } from '@/types/chat';
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
|
||||
/* knowledge base */
|
||||
export const getKbList = (parentId?: string) =>
|
||||
GET<KbListItemType[]>(`/plugins/kb/list`, { parentId });
|
||||
export const getKbList = (data: { parentId?: string; type?: `${KbTypeEnum}` }) =>
|
||||
GET<KbListItemType[]>(`/plugins/kb/list`, data);
|
||||
export const getAllDataset = () => GET<KbListItemType[]>(`/plugins/kb/allDataset`);
|
||||
|
||||
export const getKbPaths = (parentId?: string) =>
|
||||
@@ -36,16 +30,6 @@ export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, dat
|
||||
|
||||
export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`);
|
||||
|
||||
/* kb file */
|
||||
export const getKbFiles = (data: { kbId: string; searchText: string }) =>
|
||||
GET<KbFileItemType[]>(`/plugins/kb/file/list`, data);
|
||||
export const deleteKbFileById = (params: { fileId: string; kbId: string }) =>
|
||||
DELETE(`/plugins/kb/file/delFileByFileId`, params);
|
||||
export const getFileInfoById = (fileId: string) =>
|
||||
GET<FileInfo>(`/plugins/kb/file/getFileInfo`, { fileId });
|
||||
export const delEmptyFiles = (kbId: string) =>
|
||||
DELETE(`/plugins/kb/file/deleteEmptyFiles`, { kbId });
|
||||
|
||||
/* kb data */
|
||||
export const getKbDataList = (data: GetKbDataListProps) =>
|
||||
POST(`/plugins/kb/data/getDataList`, data);
|
||||
@@ -53,7 +37,7 @@ export const getKbDataList = (data: GetKbDataListProps) =>
|
||||
/**
|
||||
* 获取导出数据(不分页)
|
||||
*/
|
||||
export const getExportDataList = (data: { kbId: string; fileId: string }) =>
|
||||
export const getExportDataList = (data: { kbId: string }) =>
|
||||
GET<[string, string, string][]>(`/plugins/kb/data/exportModelData`, data, {
|
||||
timeout: 600000
|
||||
});
|
||||
|
||||
1
client/src/api/request/kb.d.ts
vendored
1
client/src/api/request/kb.d.ts
vendored
@@ -3,6 +3,7 @@ import type { RequestPaging } from '@/types';
|
||||
|
||||
export type KbUpdateParams = {
|
||||
id: string;
|
||||
parentId?: string;
|
||||
tags?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { baseUrl } from '../../service/lib/openai';
|
||||
|
||||
interface ConfigType {
|
||||
headers?: { [key: string]: string };
|
||||
|
||||
18
client/src/api/support/file.ts
Normal file
18
client/src/api/support/file.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { GET, POST } from '../request';
|
||||
|
||||
import { AxiosProgressEvent } from 'axios';
|
||||
|
||||
export const uploadImg = (base64Img: string) => POST<string>('/system/uploadImage', { base64Img });
|
||||
|
||||
export const postUploadFiles = (
|
||||
data: FormData,
|
||||
onUploadProgress: (progressEvent: AxiosProgressEvent) => void
|
||||
) =>
|
||||
POST<string[]>('/support/file/upload', data, {
|
||||
onUploadProgress,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
}
|
||||
});
|
||||
|
||||
export const getFileViewUrl = (fileId: string) => GET<string>('/support/file/readUrl', { fileId });
|
||||
34
client/src/api/support/outLink.ts
Normal file
34
client/src/api/support/outLink.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { GET, POST, DELETE } from '../request';
|
||||
import type { InitShareChatResponse } from '../response/chat';
|
||||
import type { OutLinkEditType } from '@/types/support/outLink';
|
||||
import type { OutLinkSchema } from '@/types/support/outLink';
|
||||
|
||||
/**
|
||||
* 初始化分享聊天
|
||||
*/
|
||||
export const initShareChatInfo = (data: { shareId: string }) =>
|
||||
GET<InitShareChatResponse>(`/support/outLink/init`, data);
|
||||
|
||||
/**
|
||||
* create a shareChat
|
||||
*/
|
||||
export const createShareChat = (
|
||||
data: OutLinkEditType & {
|
||||
appId: string;
|
||||
type: OutLinkSchema['type'];
|
||||
}
|
||||
) => POST<string>(`/support/outLink/create`, data);
|
||||
|
||||
export const putShareChat = (data: OutLinkEditType) =>
|
||||
POST<string>(`/support/outLink/update`, data);
|
||||
|
||||
/**
|
||||
* get shareChat
|
||||
*/
|
||||
export const getShareChatList = (appId: string) =>
|
||||
GET<OutLinkSchema[]>(`/support/outLink/list`, { appId });
|
||||
|
||||
/**
|
||||
* delete a shareChat
|
||||
*/
|
||||
export const delShareChatById = (id: string) => DELETE(`/support/outLink/delete?id=${id}`);
|
||||
@@ -1,20 +1,4 @@
|
||||
import { GET, POST, PUT } from './request';
|
||||
import type { InitDateResponse } from '@/pages/api/system/getInitData';
|
||||
import { AxiosProgressEvent } from 'axios';
|
||||
|
||||
export const getInitData = () => GET<InitDateResponse>('/system/getInitData');
|
||||
|
||||
export const uploadImg = (base64Img: string) => POST<string>('/system/uploadImage', { base64Img });
|
||||
|
||||
export const postUploadFiles = (
|
||||
data: FormData,
|
||||
onUploadProgress: (progressEvent: AxiosProgressEvent) => void
|
||||
) =>
|
||||
POST<string[]>('/plugins/file/upload', data, {
|
||||
onUploadProgress,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
}
|
||||
});
|
||||
|
||||
export const getFileViewUrl = (fileId: string) => GET<string>('/plugins/file/readUrl', { fileId });
|
||||
|
||||
@@ -5,16 +5,21 @@ import { UserAuthTypeEnum } from '@/constants/common';
|
||||
import { UserBillType, UserType, UserUpdateParams } from '@/types/user';
|
||||
import type { PagingData, RequestPaging } from '@/types';
|
||||
import { informSchema, PaySchema } from '@/types/mongoSchema';
|
||||
import { OAuthEnum } from '@/constants/user';
|
||||
|
||||
export const sendAuthCode = (data: {
|
||||
username: string;
|
||||
type: `${UserAuthTypeEnum}`;
|
||||
googleToken: string;
|
||||
}) => POST(`/plusApi/user/account/sendCode`, data);
|
||||
}) => POST(`/plusApi/user/inform/sendAuthCode`, data);
|
||||
|
||||
export const getTokenLogin = () => GET<UserType>('/user/account/tokenLogin');
|
||||
export const gitLogin = (params: { code: string; inviterId?: string }) =>
|
||||
GET<ResLogin>('/plusApi/user/account/gitLogin', params);
|
||||
export const oauthLogin = (params: {
|
||||
type: `${OAuthEnum}`;
|
||||
code: string;
|
||||
callbackUrl: string;
|
||||
inviterId?: string;
|
||||
}) => POST<ResLogin>('/plusApi/user/account/login/oauth', params);
|
||||
|
||||
export const postRegister = ({
|
||||
username,
|
||||
@@ -27,7 +32,7 @@ export const postRegister = ({
|
||||
password: string;
|
||||
inviterId?: string;
|
||||
}) =>
|
||||
POST<ResLogin>(`/plusApi/user/account/register`, {
|
||||
POST<ResLogin>(`/plusApi/user/account/register/emailAndPhone`, {
|
||||
username,
|
||||
code,
|
||||
inviterId,
|
||||
@@ -43,7 +48,7 @@ export const postFindPassword = ({
|
||||
code: string;
|
||||
password: string;
|
||||
}) =>
|
||||
POST<ResLogin>(`/plusApi/user/account/updatePasswordByCode`, {
|
||||
POST<ResLogin>(`/plusApi/user/account/password/updateByCode`, {
|
||||
username,
|
||||
code,
|
||||
password: createHashPassword(password)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { ModalBody, Box, useTheme } from '@chakra-ui/react';
|
||||
import { getKbDataItemById } from '@/api/plugins/kb';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
@@ -9,6 +9,7 @@ import MyIcon from '@/components/Icon';
|
||||
import InputDataModal, { RawFileText } from '@/pages/kb/detail/components/InputDataModal';
|
||||
import MyModal from '../MyModal';
|
||||
import { KbDataItemType } from '@/types/plugin';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type SearchType = KbDataItemType & {
|
||||
kb_id?: string;
|
||||
@@ -24,10 +25,13 @@ const QuoteModal = ({
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { setIsLoading, Loading } = useLoading();
|
||||
const [editDataItem, setEditDataItem] = useState<QuoteItemType>();
|
||||
|
||||
const isShare = useMemo(() => router.pathname === '/chat/share', [router.pathname]);
|
||||
|
||||
/**
|
||||
* click edit, get new kbDataItem
|
||||
*/
|
||||
@@ -91,10 +95,12 @@ const QuoteModal = ({
|
||||
_hover={{ '& .edit': { display: 'flex' } }}
|
||||
overflow={'hidden'}
|
||||
>
|
||||
{item.source && <RawFileText filename={item.source} fileId={item.file_id} />}
|
||||
{item.source && !isShare && (
|
||||
<RawFileText filename={item.source} fileId={item.file_id} />
|
||||
)}
|
||||
<Box>{item.q}</Box>
|
||||
<Box>{item.a}</Box>
|
||||
{item.id && (
|
||||
{item.id && !isShare && (
|
||||
<Box
|
||||
className="edit"
|
||||
display={'none'}
|
||||
|
||||
1
client/src/components/Icon/icons/fill/google.svg
Normal file
1
client/src/components/Icon/icons/fill/google.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694437679570" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7334" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M214.101333 512c0-32.512 5.546667-63.701333 15.36-92.928L57.173333 290.218667A491.861333 491.861333 0 0 0 4.693333 512c0 79.701333 18.858667 154.88 52.394667 221.610667l172.202667-129.066667A290.56 290.56 0 0 1 214.101333 512" fill="#FBBC05" p-id="7335"></path><path d="M516.693333 216.192c72.106667 0 137.258667 25.002667 188.458667 65.962667L854.101333 136.533333C763.349333 59.178667 646.997333 11.392 516.693333 11.392c-202.325333 0-376.234667 113.28-459.52 278.826667l172.373334 128.853333c39.68-118.016 152.832-202.88 287.146666-202.88" fill="#EA4335" p-id="7336"></path><path d="M516.693333 807.808c-134.357333 0-247.509333-84.864-287.232-202.88l-172.288 128.853333c83.242667 165.546667 257.152 278.826667 459.52 278.826667 124.842667 0 244.053333-43.392 333.568-124.757333l-163.584-123.818667c-46.122667 28.458667-104.234667 43.776-170.026666 43.776" fill="#34A853" p-id="7337"></path><path d="M1005.397333 512c0-29.568-4.693333-61.44-11.648-91.008H516.650667V614.4h274.602666c-13.696 65.962667-51.072 116.650667-104.533333 149.632l163.541333 123.818667c93.994667-85.418667 155.136-212.650667 155.136-375.850667" fill="#4285F4" p-id="7338"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
client/src/components/Icon/icons/light/move.svg
Normal file
1
client/src/components/Icon/icons/light/move.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694403033666" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4053" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M1016.32 494.08l-143.872-143.36c-9.728-9.728-26.112-9.728-35.84 0-9.728 9.728-9.728 26.112 0 35.84L936.96 486.4h-399.36V87.04l99.84 100.352c9.728 9.728 26.112 9.728 35.84 0 9.728-9.728 9.728-26.112 0-35.84l-143.36-143.872c-9.728-9.728-26.112-9.728-35.84 0l-143.36 143.872c-9.728 9.728-9.728 26.112 0 35.84 9.728 9.728 26.112 9.728 35.84 0L486.4 87.04v399.36H87.04l100.352-99.84c9.728-9.728 9.728-26.112 0-35.84-9.728-9.728-26.112-9.728-35.84 0l-143.872 143.36c-9.728 9.728-9.728 26.112 0 35.84l143.872 143.36c9.728 9.728 26.112 9.728 35.84 0 9.728-9.728 9.728-26.112 0-35.84L87.04 537.6h399.36v399.36l-99.84-100.352c-9.728-9.728-26.112-9.728-35.84 0-9.728 9.728-9.728 26.112 0 35.84l143.36 143.872c9.728 9.728 26.112 9.728 35.84 0l143.36-143.872c9.728-9.728 9.728-26.112 0-35.84-9.728-9.728-26.112-9.728-35.84 0L537.6 936.96v-399.36h399.36l-100.352 99.84c-9.728 9.728-9.728 26.112 0 35.84 9.728 9.728 26.112 9.728 35.84 0l143.872-143.36c10.24-9.728 10.24-26.112 0-35.84z" p-id="4054"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M8.3 5.7a1 1 0 011.4-1.4l7.71 7.7-7.7 7.7a1 1 0 11-1.42-1.4l6.3-6.3-6.3-6.3z" fill-rule="nonzero"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B After Width: | Height: | Size: 151 B |
@@ -24,6 +24,7 @@ const map = {
|
||||
out: require('./icons/out.svg').default,
|
||||
git: require('./icons/git.svg').default,
|
||||
gitFill: require('./icons/fill/git.svg').default,
|
||||
googleFill: require('./icons/fill/google.svg').default,
|
||||
menu: require('./icons/menu.svg').default,
|
||||
edit: require('./icons/edit.svg').default,
|
||||
inform: require('./icons/inform.svg').default,
|
||||
@@ -83,7 +84,8 @@ const map = {
|
||||
retryLight: require('./icons/light/retry.svg').default,
|
||||
rightArrowLight: require('./icons/light/rightArrow.svg').default,
|
||||
searchLight: require('./icons/light/search.svg').default,
|
||||
plusFill: require('./icons/fill/plus.svg').default
|
||||
plusFill: require('./icons/fill/plus.svg').default,
|
||||
moveLight: require('./icons/light/move.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
||||
@@ -9,7 +9,6 @@ import NextLink from 'next/link';
|
||||
import Badge from '../Badge';
|
||||
import Avatar from '../Avatar';
|
||||
import MyIcon from '../Icon';
|
||||
import Language from '../Language';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
@@ -109,7 +108,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
<Avatar
|
||||
w={'36px'}
|
||||
h={'36px'}
|
||||
borderRadius={'none'}
|
||||
borderRadius={'50%'}
|
||||
src={userInfo?.avatar}
|
||||
fallbackSrc={HUMAN_ICON}
|
||||
/>
|
||||
@@ -157,7 +156,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
<Link
|
||||
as={NextLink}
|
||||
{...itemStyles}
|
||||
href={`/account?type=inform`}
|
||||
href={`/account?currentTab=inform`}
|
||||
mb={0}
|
||||
color={'#9096a5'}
|
||||
>
|
||||
@@ -181,7 +180,6 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Language {...itemStyles} />
|
||||
{feConfigs?.show_git && (
|
||||
<MyTooltip label={`Git Star: ${gitStar}`} placement={'right-end'}>
|
||||
<Link
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props extends InputProps {
|
||||
const MyInput = ({ leftIcon, ...props }: Props) => {
|
||||
return (
|
||||
<Flex position={'relative'} alignItems={'center'}>
|
||||
<Input w={'100%'} pl={leftIcon ? '30px' : 3} {...props} />
|
||||
<Input w={'100%'} pl={leftIcon ? '30px !important' : 3} {...props} />
|
||||
{leftIcon && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
menuList: {
|
||||
isActive?: boolean;
|
||||
child: React.ReactNode;
|
||||
onClick: () => void;
|
||||
onClick: () => any;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ const MyMenu = ({ width, offset = [0, 10], Button, menuList }: Props) => {
|
||||
<MenuItem
|
||||
key={i}
|
||||
{...menuItemStyles}
|
||||
onClick={item.onClick}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
item.onClick && item.onClick();
|
||||
}}
|
||||
color={item.isActive ? 'hover.blue' : ''}
|
||||
>
|
||||
{item.child}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import type { OutLinkEditType } from '@/types/support/outLink';
|
||||
|
||||
export const defaultApp: AppSchema = {
|
||||
_id: '',
|
||||
@@ -17,6 +17,11 @@ export const defaultApp: AppSchema = {
|
||||
modules: []
|
||||
};
|
||||
|
||||
export const defaultShareChat: ShareChatEditType = {
|
||||
name: ''
|
||||
export const defaultOutLinkForm: OutLinkEditType = {
|
||||
name: '',
|
||||
responseDetail: false,
|
||||
limit: {
|
||||
QPM: 100,
|
||||
credit: -1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ export const TrainingTypeMap = {
|
||||
[TrainingModeEnum.index]: 'index'
|
||||
};
|
||||
|
||||
export const PgTrainingTableName = 'modeldata';
|
||||
export const PgDatasetTableName = 'modeldata';
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export enum OAuthEnum {
|
||||
github = 'github',
|
||||
google = 'google'
|
||||
}
|
||||
export enum BillSourceEnum {
|
||||
fastgpt = 'fastgpt',
|
||||
api = 'api',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { ModalFooter, ModalBody, Input, useDisclosure, Button } from '@chakra-ui/react';
|
||||
import MyModal from '@/components/MyModal';
|
||||
|
||||
export const useEditInfo = ({
|
||||
export const useEditTitle = ({
|
||||
title,
|
||||
placeholder = ''
|
||||
}: {
|
||||
@@ -39,6 +39,7 @@ export const useEditInfo = ({
|
||||
try {
|
||||
const val = inputRef.current.value;
|
||||
await onSuccessCb.current?.(val);
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
onErrorCb.current?.(err);
|
||||
@@ -1,5 +1,17 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { Box, Flex, Button, useDisclosure, useTheme, Divider, Select } from '@chakra-ui/react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
useDisclosure,
|
||||
useTheme,
|
||||
Divider,
|
||||
Select,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
@@ -9,13 +21,17 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { compressImg } from '@/utils/file';
|
||||
import { feConfigs } from '@/store/static';
|
||||
import { feConfigs, systemVersion } from '@/store/static';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { timezoneList } from '@/utils/user';
|
||||
import Loading from '@/components/Loading';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { getLangStore, LangEnum, langMap, setLangStore } from '@/utils/i18n';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyMenu from '@/components/MyMenu';
|
||||
import MySelect from '@/components/Select';
|
||||
|
||||
const PayModal = dynamic(() => import('./PayModal'), {
|
||||
loading: () => <Loading fixed={false} />,
|
||||
@@ -32,7 +48,8 @@ const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'), {
|
||||
|
||||
const UserInfo = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
|
||||
const timezones = useRef(timezoneList());
|
||||
const { reset } = useForm<UserUpdateParams>({
|
||||
@@ -57,6 +74,8 @@ const UserInfo = () => {
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const [language, setLanguage] = useState<`${LangEnum}`>(getLangStore());
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserType) => {
|
||||
await updateUserInfo({
|
||||
@@ -124,10 +143,12 @@ const UserInfo = () => {
|
||||
h={['44px', '54px']}
|
||||
borderRadius={'50%'}
|
||||
border={theme.borders.base}
|
||||
overflow={'hidden'}
|
||||
p={'2px'}
|
||||
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
|
||||
mb={2}
|
||||
>
|
||||
<Avatar src={userInfo?.avatar} w={'100%'} h={'100%'} />
|
||||
<Avatar src={userInfo?.avatar} borderRadius={'50%'} w={'100%'} h={'100%'} />
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
|
||||
@@ -147,6 +168,25 @@ const UserInfo = () => {
|
||||
<Box flex={'0 0 80px'}>{t('user.Account')}: </Box>
|
||||
<Box flex={1}>{userInfo?.username}</Box>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Language')}: </Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect
|
||||
value={language}
|
||||
list={Object.entries(langMap).map(([key, lang]) => ({
|
||||
label: lang.label,
|
||||
value: key
|
||||
}))}
|
||||
onchange={(val: any) => {
|
||||
const lang = val;
|
||||
setLangStore(lang);
|
||||
setLanguage(lang);
|
||||
i18n?.changeLanguage?.(lang);
|
||||
router.reload();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Timezone')}: </Box>
|
||||
<Select
|
||||
@@ -170,20 +210,49 @@ const UserInfo = () => {
|
||||
{t('user.Change')}
|
||||
</Button>
|
||||
</Flex>
|
||||
{feConfigs?.show_userDetail && (
|
||||
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Balance')}: </Box>
|
||||
<Box flex={1}>
|
||||
<strong>{userInfo?.balance.toFixed(3)}</strong> 元
|
||||
</Box>
|
||||
<Button size={['sm', 'md']} ml={5} onClick={onOpenPayModal}>
|
||||
{t('user.Pay')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
{feConfigs?.show_doc && (
|
||||
<>
|
||||
<Flex
|
||||
mt={4}
|
||||
w={['85%', '300px']}
|
||||
py={3}
|
||||
px={6}
|
||||
border={theme.borders.sm}
|
||||
borderWidth={'1.5px'}
|
||||
borderRadius={'md'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
onClick={() => {
|
||||
window.open(`https://doc.fastgpt.run/docs/intro`);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'courseLight'} w={'18px'} />
|
||||
<Box ml={2} flex={1}>
|
||||
{t('system.Help Document')}
|
||||
</Box>
|
||||
<Box w={'8px'} h={'8px'} borderRadius={'50%'} bg={'#67c13b'} />
|
||||
<Box fontSize={'md'} ml={2}>
|
||||
V{systemVersion}
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{feConfigs?.show_userDetail && (
|
||||
<>
|
||||
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Balance')}: </Box>
|
||||
<Box flex={1}>
|
||||
<strong>{userInfo?.balance.toFixed(3)}</strong> 元
|
||||
</Box>
|
||||
<Button size={['sm', 'md']} ml={5} onClick={onOpenPayModal}>
|
||||
{t('user.Pay')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<Divider my={3} />
|
||||
|
||||
<MyTooltip label={'点击配置账号'}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
@@ -12,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { rowCount } = await PgClient.query(`SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = '${PgTrainingTableName}'
|
||||
AND table_name = '${PgDatasetTableName}'
|
||||
AND column_name = 'file_id'`);
|
||||
|
||||
if (rowCount > 0) {
|
||||
@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
jsonRes(res, {
|
||||
data: await PgClient.query(
|
||||
`ALTER TABLE ${PgTrainingTableName} ADD COLUMN file_id VARCHAR(100)`
|
||||
`ALTER TABLE ${PgDatasetTableName} ADD COLUMN file_id VARCHAR(100)`
|
||||
)
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, KB } from '@/service/mongo';
|
||||
import { KbTypeEnum, KbTypeMap } from '@/constants/kb';
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
@@ -22,7 +24,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res, {});
|
||||
const response = await PgClient.update(PgDatasetTableName, {
|
||||
where: [['file_id', 'undefined']],
|
||||
values: [{ key: 'file_id', value: '' }]
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: response.rowCount
|
||||
});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
|
||||
35
client/src/pages/api/admin/initv441.ts
Normal file
35
client/src/pages/api/admin/initv441.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import mongoose from 'mongoose';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
await authUser({ req, authRoot: true });
|
||||
|
||||
const data = await mongoose.connection.db
|
||||
.collection('dataset.files')
|
||||
.updateMany({}, { $set: { 'metadata.datasetUsed': true } });
|
||||
|
||||
// update pg data
|
||||
const pg = await PgClient.query(`UPDATE ${PgDatasetTableName}
|
||||
SET file_id = ''
|
||||
WHERE (file_id = 'undefined' OR LENGTH(file_id) < 20) AND file_id != '';`);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
data,
|
||||
pg
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { Types } from 'mongoose';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
if (fileId === OtherFileId) {
|
||||
await PgClient.delete(PgTrainingTableName, {
|
||||
await PgClient.delete(PgDatasetTableName, {
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
@@ -37,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
await gridFs.findAndAuthFile(fileId);
|
||||
|
||||
// delete all pg data
|
||||
await PgClient.delete(PgTrainingTableName, {
|
||||
await PgClient.delete(PgDatasetTableName, {
|
||||
where: [['user_id', userId], 'AND', ['kb_id', kbId], 'AND', ['file_id', fileId]]
|
||||
});
|
||||
|
||||
31
client/src/pages/api/core/dataset/file/delEmptyFiles.ts
Normal file
31
client/src/pages/api/core/dataset/file/delEmptyFiles.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { kbId } = req.query as { kbId: string };
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const collection = gridFs.Collection();
|
||||
|
||||
const files = await collection.deleteMany({
|
||||
uploadDate: { $lte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
|
||||
['metadata.kbId']: kbId,
|
||||
['metadata.userId']: userId,
|
||||
['metadata.datasetUsed']: { $ne: true }
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: files
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res);
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,49 @@ import { connectToDatabase, TrainingData } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { KbFileItemType } from '@/types/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { FileStatusEnum, OtherFileId } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
let { kbId, searchText } = req.query as { kbId: string; searchText: string };
|
||||
searchText = searchText.replace(/'/g, '');
|
||||
let {
|
||||
pageNum = 1,
|
||||
pageSize = 10,
|
||||
kbId,
|
||||
searchText = ''
|
||||
} = req.body as { pageNum: number; pageSize: number; kbId: string; searchText: string };
|
||||
searchText = searchText?.replace(/'/g, '');
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
// find files
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const bucket = gridFs.GridFSBucket();
|
||||
const collection = gridFs.Collection();
|
||||
|
||||
const files = await bucket
|
||||
.find({ ['metadata.kbId']: kbId, ...(searchText && { filename: { $regex: searchText } }) })
|
||||
.sort({ _id: -1 })
|
||||
.toArray();
|
||||
const mongoWhere = {
|
||||
['metadata.kbId']: kbId,
|
||||
['metadata.userId']: userId,
|
||||
['metadata.datasetUsed']: true,
|
||||
...(searchText && { filename: { $regex: searchText } })
|
||||
};
|
||||
const [files, total] = await Promise.all([
|
||||
collection
|
||||
.find(mongoWhere, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
filename: 1,
|
||||
uploadDate: 1,
|
||||
length: 1
|
||||
}
|
||||
})
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
.toArray(),
|
||||
collection.countDocuments(mongoWhere)
|
||||
]);
|
||||
|
||||
async function GetOtherData() {
|
||||
return {
|
||||
@@ -35,7 +57,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
status: (await TrainingData.findOne({ userId, kbId, file_id: '' }))
|
||||
? FileStatusEnum.embedding
|
||||
: FileStatusEnum.ready,
|
||||
chunkLength: await PgClient.count(PgTrainingTableName, {
|
||||
chunkLength: await PgClient.count(PgDatasetTableName, {
|
||||
fields: ['id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
@@ -58,7 +80,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
status: (await TrainingData.findOne({ userId, kbId, file_id: file._id }))
|
||||
? FileStatusEnum.embedding
|
||||
: FileStatusEnum.ready,
|
||||
chunkLength: await PgClient.count(PgTrainingTableName, {
|
||||
chunkLength: await PgClient.count(PgDatasetTableName, {
|
||||
fields: ['id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
@@ -72,8 +94,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
})
|
||||
]);
|
||||
|
||||
jsonRes<KbFileItemType[]>(res, {
|
||||
data: data.flat().filter((item) => item.chunkLength > 0)
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data: data.flat(),
|
||||
total
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
66
client/src/pages/api/core/dataset/file/update.ts
Normal file
66
client/src/pages/api/core/dataset/file/update.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { UpdateFileProps } from '@/api/core/dataset/file.d';
|
||||
import { Types } from 'mongoose';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { id, name, datasetUsed } = req.body as UpdateFileProps;
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const collection = gridFs.Collection();
|
||||
|
||||
await collection.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(id)
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
...(name && { filename: name }),
|
||||
...(datasetUsed && { ['metadata.datasetUsed']: datasetUsed })
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// data source
|
||||
updateDatasetSource({
|
||||
fileId: id,
|
||||
userId,
|
||||
name
|
||||
});
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
async function updateDatasetSource(data: { fileId: string; userId: string; name?: string }) {
|
||||
const { fileId, userId, name } = data;
|
||||
if (!fileId || !name || !userId) return;
|
||||
try {
|
||||
await PgClient.update(PgDatasetTableName, {
|
||||
where: [['user_id', userId], 'AND', ['file_id', fileId]],
|
||||
values: [
|
||||
{
|
||||
key: 'source',
|
||||
value: name
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
updateDatasetSource(data);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
@@ -18,7 +18,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
await PgClient.delete(PgTrainingTableName, {
|
||||
await PgClient.delete(PgDatasetTableName, {
|
||||
where: [['user_id', userId], 'AND', ['id', dataId]]
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { connectToDatabase, TrainingData, KB } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { authKb } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgTrainingTableName, TrainingModeEnum } from '@/constants/plugin';
|
||||
import { PgDatasetTableName, TrainingModeEnum } from '@/constants/plugin';
|
||||
import { startQueue } from '@/service/utils/tools';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
@@ -136,7 +136,7 @@ export async function pushDataToKb({
|
||||
try {
|
||||
const { rows } = await PgClient.query(`
|
||||
SELECT COUNT(*) > 0 AS exists
|
||||
FROM ${PgTrainingTableName}
|
||||
FROM ${PgDatasetTableName}
|
||||
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
|
||||
`);
|
||||
const exists = rows[0]?.exists || false;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PgClient } from '@/service/pg';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { getVector } from '../plugin/vector';
|
||||
import type { KbTestItemType } from '@/types/plugin';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { KB } from '@/service/mongo';
|
||||
|
||||
export type Props = {
|
||||
@@ -43,7 +43,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
|
||||
select id, q, a, source, file_id, (vector <#> '[${
|
||||
vectors[0]
|
||||
}]') * -1 AS score from ${PgTrainingTableName} where kb_id='${kbId}' AND user_id='${userId}' order by vector <#> '[${
|
||||
}]') * -1 AS score from ${PgDatasetTableName} where kb_id='${kbId}' AND user_id='${userId}' order by vector <#> '[${
|
||||
vectors[0]
|
||||
}]' limit 12;
|
||||
COMMIT;`
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PgClient } from '@/service/pg';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { KB, connectToDatabase } from '@/service/mongo';
|
||||
import { getVector } from '../plugin/vector';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export type Props = {
|
||||
dataId: string;
|
||||
@@ -47,7 +47,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
})();
|
||||
|
||||
// 更新 pg 内容.仅修改a,不需要更新向量。
|
||||
await PgClient.update(PgTrainingTableName, {
|
||||
await PgClient.update(PgDatasetTableName, {
|
||||
where: [['id', dataId], 'AND', ['user_id', userId]],
|
||||
values: [
|
||||
{ key: 'a', value: a.replace(/'/g, '"') },
|
||||
|
||||
@@ -68,6 +68,7 @@ export async function getVector({
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.data?.data?.[0]?.embedding) {
|
||||
console.log(res.data);
|
||||
// @ts-ignore
|
||||
return Promise.reject(res.data?.err?.message || 'Embedding API Error');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser, authApp, authShareChat, AuthUserTypeEnum } from '@/service/utils/auth';
|
||||
import { authUser, authApp } from '@/service/utils/auth';
|
||||
import { sseErrRes, jsonRes } from '@/service/response';
|
||||
import { addLog, withNextCors } from '@/service/utils/tools';
|
||||
import { ChatRoleEnum, ChatSourceEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||
@@ -29,6 +29,8 @@ import { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { getSystemTime } from '@/utils/user';
|
||||
import { authOutLinkChat } from '@/service/support/outLink/auth';
|
||||
import requestIp from 'request-ip';
|
||||
|
||||
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
|
||||
type FastGptWebChatProps = {
|
||||
@@ -82,14 +84,17 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
let startTime = Date.now();
|
||||
|
||||
/* user auth */
|
||||
const {
|
||||
let {
|
||||
// @ts-ignore
|
||||
responseDetail,
|
||||
user,
|
||||
userId,
|
||||
appId: authAppid,
|
||||
authType
|
||||
} = await (shareId
|
||||
? authShareChat({
|
||||
shareId
|
||||
? authOutLinkChat({
|
||||
shareId,
|
||||
ip: requestIp.getClientIp(req)
|
||||
})
|
||||
: authUser({ req, authBalance: true }));
|
||||
|
||||
@@ -112,6 +117,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
]);
|
||||
|
||||
const isOwner = !shareId && userId === String(app.userId);
|
||||
responseDetail = isOwner || responseDetail;
|
||||
|
||||
const prompts = history.concat(gptMessage2ChatType(messages));
|
||||
if (prompts[prompts.length - 1].obj === 'AI') {
|
||||
@@ -157,7 +163,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
appId,
|
||||
userId,
|
||||
variables,
|
||||
isOwner,
|
||||
isOwner, // owner update use time
|
||||
shareId,
|
||||
source: (() => {
|
||||
if (shareId) {
|
||||
@@ -197,7 +203,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
data: '[DONE]'
|
||||
});
|
||||
|
||||
if (isOwner && detail) {
|
||||
if (responseDetail && detail) {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.appStreamResponse,
|
||||
|
||||
@@ -3,14 +3,13 @@ import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, User } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { findAllChildrenIds } from '../delete';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let { kbId, fileId } = req.query as {
|
||||
let { kbId } = req.query as {
|
||||
kbId: string;
|
||||
fileId: string;
|
||||
};
|
||||
|
||||
if (!kbId) {
|
||||
@@ -22,8 +21,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const exportIds = [kbId, ...(await findAllChildrenIds(kbId))];
|
||||
console.log(exportIds);
|
||||
|
||||
const thirtyMinutesAgo = new Date(
|
||||
Date.now() - (global.feConfigs?.exportLimitMinutes || 0) * 60 * 1000
|
||||
Date.now() - (global.feConfigs?.limit?.exportLimitMinutes || 0) * 60 * 1000
|
||||
);
|
||||
|
||||
// auth export times
|
||||
@@ -39,14 +41,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
);
|
||||
|
||||
if (!authTimes) {
|
||||
const minutes = `${global.feConfigs?.exportLimitMinutes || 0} 分钟`;
|
||||
const minutes = `${global.feConfigs?.limit?.exportLimitMinutes || 0} 分钟`;
|
||||
throw new Error(`上次导出未到 ${minutes},每 ${minutes}仅可导出一次。`);
|
||||
}
|
||||
|
||||
const where: any = [['kb_id', kbId], 'AND', ['user_id', userId]];
|
||||
const where: any = [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
`kb_id IN (${exportIds.map((id) => `'${id}'`).join(',')})`
|
||||
];
|
||||
// 从 pg 中获取所有数据
|
||||
const pgData = await PgClient.select<{ q: string; a: string; source: string }>(
|
||||
PgTrainingTableName,
|
||||
PgDatasetTableName,
|
||||
{
|
||||
where,
|
||||
fields: ['q', 'a', 'source'],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import type { KbDataItemType } from '@/types/plugin';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
export type Response = {
|
||||
id: string;
|
||||
@@ -29,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
const where: any = [['user_id', userId], 'AND', ['id', dataId]];
|
||||
|
||||
const searchRes = await PgClient.select<KbDataItemType>(PgTrainingTableName, {
|
||||
const searchRes = await PgClient.select<KbDataItemType>(PgDatasetTableName, {
|
||||
fields: ['kb_id', 'id', 'q', 'a', 'source', 'file_id'],
|
||||
where,
|
||||
limit: 1
|
||||
|
||||
@@ -4,7 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import type { KbDataItemType } from '@/types/plugin';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { OtherFileId } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -50,14 +50,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
];
|
||||
|
||||
const [searchRes, total] = await Promise.all([
|
||||
PgClient.select<KbDataItemType>(PgTrainingTableName, {
|
||||
PgClient.select<KbDataItemType>(PgDatasetTableName, {
|
||||
fields: ['id', 'q', 'a', 'source', 'file_id'],
|
||||
where,
|
||||
order: [{ field: 'id', mode: 'DESC' }],
|
||||
limit: pageSize,
|
||||
offset: pageSize * (pageNum - 1)
|
||||
}),
|
||||
PgClient.count(PgTrainingTableName, {
|
||||
PgClient.count(PgDatasetTableName, {
|
||||
fields: ['id'],
|
||||
where
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, KB } from '@/service/mongo';
|
||||
import { authKb, authUser } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { insertKbItem, PgClient } from '@/service/pg';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import { getVectorModel } from '@/service/utils/data';
|
||||
@@ -45,7 +45,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
|
||||
const { rows: existsRows } = await PgClient.query(`
|
||||
SELECT COUNT(*) > 0 AS exists
|
||||
FROM ${PgTrainingTableName}
|
||||
FROM ${PgDatasetTableName}
|
||||
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
|
||||
`);
|
||||
const exists = existsRows[0]?.exists || false;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, KB, App, TrainingData } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -29,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
});
|
||||
|
||||
// delete all pg data
|
||||
await PgClient.delete(PgTrainingTableName, {
|
||||
await PgClient.delete(PgDatasetTableName, {
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
@@ -56,7 +56,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
}
|
||||
}
|
||||
|
||||
async function findAllChildrenIds(id: string) {
|
||||
export async function findAllChildrenIds(id: string) {
|
||||
// find children
|
||||
const children = await KB.find({ parentId: id });
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { kbId } = req.query as { kbId: string };
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const gridFs = new GridFSStorage('dataset', userId);
|
||||
const bucket = gridFs.GridFSBucket();
|
||||
|
||||
const files = await bucket
|
||||
// 1 hours expired
|
||||
.find({
|
||||
uploadDate: { $lte: new Date(Date.now() - 60 * 1000) },
|
||||
['metadata.kbId']: kbId,
|
||||
['metadata.userId']: userId
|
||||
})
|
||||
.sort({ _id: -1 })
|
||||
.toArray();
|
||||
|
||||
const data = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
return {
|
||||
id: file._id,
|
||||
chunkLength: await PgClient.count(PgTrainingTableName, {
|
||||
fields: ['id'],
|
||||
where: [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['kb_id', kbId],
|
||||
'AND',
|
||||
['file_id', String(file._id)]
|
||||
]
|
||||
})
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
data
|
||||
.filter((item) => item.chunkLength === 0)
|
||||
.map((file) => bucket.delete(new Types.ObjectId(file.id)))
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import { connectToDatabase, KB } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { getVectorModel } from '@/service/utils/data';
|
||||
import { KbListItemType } from '@/types/plugin';
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { parentId } = req.query as { parentId: string };
|
||||
const { parentId, type } = req.query as { parentId?: string; type?: `${KbTypeEnum}` };
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
@@ -15,7 +16,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
const kbList = await KB.find({
|
||||
userId,
|
||||
parentId: parentId || null
|
||||
...(parentId !== undefined && { parentId: parentId || null }),
|
||||
...(type && { type })
|
||||
}).sort({ updateTime: -1 });
|
||||
|
||||
const data = await Promise.all(
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { KbUpdateParams } from '@/api/request/kb';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { id, name, avatar, tags = '' } = req.body as KbUpdateParams;
|
||||
const { id, parentId, name, avatar, tags } = req.body as KbUpdateParams;
|
||||
|
||||
if (!id || !name) {
|
||||
if (!id) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
@@ -23,9 +23,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
userId
|
||||
},
|
||||
{
|
||||
...(parentId !== undefined && { parentId: parentId || null }),
|
||||
...(name && { name }),
|
||||
...(avatar && { avatar }),
|
||||
tags: tags.split(' ').filter((item) => item)
|
||||
...(typeof tags === 'string' && {
|
||||
tags: tags.split(' ').filter((item) => item)
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: `/api/plugins/file/read?token=${token}`
|
||||
data: `/api/support/file/read?token=${token}`
|
||||
});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, OutLink } from '@/service/mongo';
|
||||
import { authApp, authUser } from '@/service/utils/auth';
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import type { OutLinkEditType } from '@/types/support/outLink';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { OutLinkTypeEnum } from '@/constants/chat';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
|
||||
@@ -10,8 +10,9 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
|
||||
/* create a shareChat */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { appId, name } = req.body as ShareChatEditType & {
|
||||
const { appId, ...props } = req.body as OutLinkEditType & {
|
||||
appId: string;
|
||||
type: `${OutLinkTypeEnum}`;
|
||||
};
|
||||
|
||||
await connectToDatabase();
|
||||
@@ -28,8 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
shareId,
|
||||
userId,
|
||||
appId,
|
||||
name,
|
||||
type: OutLinkTypeEnum.share
|
||||
...props
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
@@ -6,12 +6,12 @@ import { authUser } from '@/service/utils/auth';
|
||||
/* delete a shareChat by shareChatId */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { id } = req.query as {
|
||||
id: string;
|
||||
};
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await OutLink.findOneAndRemove({
|
||||
@@ -6,7 +6,7 @@ import { authApp } from '@/service/utils/auth';
|
||||
import { HUMAN_ICON } from '@/constants/chat';
|
||||
import { getChatModelNameList, getSpecialModule } from '@/components/ChatBox/utils';
|
||||
|
||||
/* 初始化我的聊天框,需要身份验证 */
|
||||
/* init share chat window */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
let { shareId } = req.query as {
|
||||
@@ -7,12 +7,12 @@ import { hashPassword } from '@/service/utils/tools';
|
||||
/* get shareChat list by appId */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { appId } = req.query as {
|
||||
appId: string;
|
||||
};
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const data = await OutLink.find({
|
||||
@@ -22,15 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
_id: -1
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: data.map((item) => ({
|
||||
_id: item._id,
|
||||
shareId: item.shareId,
|
||||
name: item.name,
|
||||
total: item.total,
|
||||
lastTime: item.lastTime
|
||||
}))
|
||||
});
|
||||
jsonRes(res, { data });
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
25
client/src/pages/api/support/outLink/update.ts
Normal file
25
client/src/pages/api/support/outLink/update.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, OutLink } from '@/service/mongo';
|
||||
import type { OutLinkEditType } from '@/types/support/outLink';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { _id, name, responseDetail, limit } = req.body as OutLinkEditType & {};
|
||||
|
||||
await OutLink.findByIdAndUpdate(_id, {
|
||||
name,
|
||||
responseDetail,
|
||||
limit
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export type InitDateResponse = {
|
||||
qaModel: QAModelItemType;
|
||||
vectorModels: VectorModelItemType[];
|
||||
feConfigs: FeConfigsType;
|
||||
systemVersion: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -24,7 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
feConfigs: global.feConfigs,
|
||||
chatModels: global.chatModels,
|
||||
qaModel: global.qaModel,
|
||||
vectorModels: global.vectorModels
|
||||
vectorModels: global.vectorModels,
|
||||
systemVersion: global.systemVersion || '0.0.0'
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -44,7 +46,9 @@ const defaultFeConfigs: FeConfigsType = {
|
||||
show_doc: true,
|
||||
systemTitle: 'FastGPT',
|
||||
authorText: 'Made by FastGPT Team.',
|
||||
exportLimitMinutes: 0,
|
||||
limit: {
|
||||
exportLimitMinutes: 0
|
||||
},
|
||||
scripts: []
|
||||
};
|
||||
const defaultChatModels = [
|
||||
@@ -94,9 +98,14 @@ export async function getInitConfig() {
|
||||
try {
|
||||
if (global.feConfigs) return;
|
||||
|
||||
getSystemVersion();
|
||||
|
||||
const filename =
|
||||
process.env.NODE_ENV === 'development' ? 'data/config.local.json' : '/app/data/config.json';
|
||||
const res = JSON.parse(readFileSync(filename, 'utf-8'));
|
||||
|
||||
console.log(`System Version: ${global.systemVersion}`);
|
||||
|
||||
console.log(res);
|
||||
|
||||
global.systemEnv = res.SystemParams
|
||||
@@ -119,3 +128,19 @@ export function setDefaultData() {
|
||||
global.qaModel = defaultQAModel;
|
||||
global.vectorModels = defaultVectorModels;
|
||||
}
|
||||
|
||||
export function getSystemVersion() {
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
global.systemVersion = process.env.npm_package_version || '0.0.0';
|
||||
return;
|
||||
}
|
||||
const packageJson = JSON.parse(readFileSync('/app/package.json', 'utf-8'));
|
||||
|
||||
global.systemVersion = packageJson?.version;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
global.systemVersion = '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,13 @@ export const KBSelectModal = ({
|
||||
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
|
||||
<ModalHeader>
|
||||
{!!parentId ? (
|
||||
<Flex flex={1}>
|
||||
<Flex
|
||||
flex={1}
|
||||
userSelect={'none'}
|
||||
fontSize={['sm', 'lg']}
|
||||
fontWeight={'normal'}
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{paths.map((item, i) => (
|
||||
<Flex key={item.parentId} mr={2} alignItems={'center'}>
|
||||
<Box
|
||||
@@ -106,7 +112,7 @@ export const KBSelectModal = ({
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== paths.length - 1 && (
|
||||
<MyIcon name={'rightArrowLight'} color={'myGray.500'} />
|
||||
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['18px', '24px']} />
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
@@ -10,45 +10,46 @@ import {
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useDisclosure,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
FormControl,
|
||||
Input,
|
||||
useTheme
|
||||
useTheme,
|
||||
Switch,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getShareChatList, delShareChatById, createShareChat } from '@/api/chat';
|
||||
import {
|
||||
getShareChatList,
|
||||
delShareChatById,
|
||||
createShareChat,
|
||||
putShareChat
|
||||
} from '@/api/support/outLink';
|
||||
import { formatTimeToChatTime, useCopyData } from '@/utils/tools';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { defaultShareChat } from '@/constants/model';
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import { defaultOutLinkForm } from '@/constants/model';
|
||||
import type { OutLinkEditType } from '@/types/support/outLink';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { OutLinkTypeEnum } from '@/constants/chat';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import MyRadio from '@/components/Radio';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const Share = ({ appId }: { appId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { copyData } = useCopyData();
|
||||
const {
|
||||
isOpen: isOpenCreateShareChat,
|
||||
onOpen: onOpenCreateShareChat,
|
||||
onClose: onCloseCreateShareChat
|
||||
} = useDisclosure();
|
||||
const {
|
||||
register: registerShareChat,
|
||||
getValues: getShareChatValues,
|
||||
setValue: setShareChatValues,
|
||||
handleSubmit: submitShareChat,
|
||||
reset: resetShareChat
|
||||
} = useForm({
|
||||
defaultValues: defaultShareChat
|
||||
});
|
||||
const [editLinkData, setEditLinkData] = useState<OutLinkEditType>();
|
||||
const { toast } = useToast();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
@@ -56,22 +57,6 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
refetch: refetchShareChatList
|
||||
} = useQuery(['initShareChatList', appId], () => getShareChatList(appId));
|
||||
|
||||
const { mutate: onclickCreateShareChat, isLoading: creating } = useRequest({
|
||||
mutationFn: async (e: ShareChatEditType) =>
|
||||
createShareChat({
|
||||
...e,
|
||||
appId
|
||||
}),
|
||||
errorToast: '创建分享链接异常',
|
||||
onSuccess(id) {
|
||||
onCloseCreateShareChat();
|
||||
refetchShareChatList();
|
||||
const url = `${location.origin}/chat/share?shareId=${id}`;
|
||||
copyData(url, '创建成功。已复制分享地址,可直接分享使用');
|
||||
resetShareChat(defaultShareChat);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position={'relative'} pt={[3, 5, 8]} px={[5, 8]} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
@@ -79,7 +64,7 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
免登录窗口
|
||||
<MyTooltip
|
||||
forceShow
|
||||
label="可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的tokens。请保管好链接和密码。"
|
||||
label="可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的余额,请保管好链接!"
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
@@ -94,7 +79,7 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
title: '最多创建10组'
|
||||
}
|
||||
: {})}
|
||||
onClick={onOpenCreateShareChat}
|
||||
onClick={() => setEditLinkData(defaultOutLinkForm)}
|
||||
>
|
||||
创建新链接
|
||||
</Button>
|
||||
@@ -105,8 +90,14 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
<Tr>
|
||||
<Th>名称</Th>
|
||||
<Th>金额消耗</Th>
|
||||
<>
|
||||
<Th>金额限制</Th>
|
||||
<Th>IP限流(人/分钟)</Th>
|
||||
</>
|
||||
<Th>返回详情</Th>
|
||||
<Th>过期时间</Th>
|
||||
<Th>最后使用时间</Th>
|
||||
<Th>操作</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -114,60 +105,90 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.name}</Td>
|
||||
<Td>{formatPrice(item.total)}元</Td>
|
||||
{item.limit && (
|
||||
<>
|
||||
<Td>{item.limit?.credit > -1 ? `${item.limit?.credit}元` : '无限制'}</Td>
|
||||
<Td>{item.limit?.QPM}</Td>
|
||||
</>
|
||||
)}
|
||||
<Td>{item.responseDetail ? '✔' : '✖'}</Td>
|
||||
<Td>
|
||||
{item.limit?.expiredTime
|
||||
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
|
||||
: '-'}
|
||||
</Td>
|
||||
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
<MyTooltip label={'嵌入网页'}>
|
||||
<MyIcon
|
||||
mr={4}
|
||||
name="apiLight"
|
||||
w={'14px'}
|
||||
<Menu autoSelect={false} isLazy>
|
||||
<MenuButton
|
||||
_hover={{ bg: 'myWhite.600 ' }}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
const src = `${location.origin}/js/iframe.js`;
|
||||
const script = `<script src="${src}" id="fastgpt-iframe" data-src="${url}" data-color="#4e83fd"></script>`;
|
||||
copyData(script, '已复制嵌入 Script,可在应用 HTML 底部嵌入', 3000);
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={'复制分享链接'}>
|
||||
<MyIcon
|
||||
mr={4}
|
||||
name="copy"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
copyData(url, '已复制分享链接,可直接分享使用');
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={'删除链接'}>
|
||||
<MyIcon
|
||||
name="delete"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red' }}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetchShareChatList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<MyIcon name={'more'} w={'14px'} p={2} />
|
||||
</MenuButton>
|
||||
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
setEditLinkData({
|
||||
_id: item._id,
|
||||
name: item.name,
|
||||
responseDetail: item.responseDetail,
|
||||
limit: item.limit
|
||||
})
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
py={[2, 3]}
|
||||
>
|
||||
<MyIcon name={'edit'} w={['14px', '16px']} />
|
||||
<Box ml={[1, 2]}>{t('common.Edit')}</Box>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
copyData(url, '已复制分享链接,可直接分享使用');
|
||||
}}
|
||||
py={[2, 3]}
|
||||
>
|
||||
<MyIcon name={'copy'} w={['14px', '16px']} />
|
||||
<Box ml={[1, 2]}>{t('common.Copy')}</Box>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
const src = `${location.origin}/js/iframe.js`;
|
||||
const script = `<script src="${src}" id="fastgpt-iframe" data-src="${url}" data-color="#4e83fd"></script>`;
|
||||
copyData(script, '已复制嵌入 Script,可在应用 HTML 底部嵌入', 3000);
|
||||
}}
|
||||
py={[2, 3]}
|
||||
>
|
||||
<MyIcon name={'apiLight'} w={['14px', '16px']} />
|
||||
<Box ml={[1, 2]}>{t('outlink.Copy Iframe')}</Box>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetchShareChatList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
py={[2, 3]}
|
||||
>
|
||||
<MyIcon name={'delete'} w={['14px', '16px']} />
|
||||
<Box ml={[1, 2]}>{t('common.Delete')}</Box>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{shareChatList.length === 0 && !isFetching && (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'10vh'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
@@ -176,56 +197,185 @@ const Share = ({ appId }: { appId: string }) => {
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{/* create shareChat modal */}
|
||||
<MyModal
|
||||
isOpen={isOpenCreateShareChat}
|
||||
onClose={onCloseCreateShareChat}
|
||||
title={'创建免登录窗口'}
|
||||
>
|
||||
<ModalBody>
|
||||
<FormControl>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'} w={0}>
|
||||
名称:
|
||||
</Box>
|
||||
<Input
|
||||
placeholder="记录名字,仅用于展示"
|
||||
maxLength={20}
|
||||
{...registerShareChat('name', {
|
||||
required: '记录名称不能为空'
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onCloseCreateShareChat}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isLoading={creating}
|
||||
onClick={submitShareChat((data) => onclickCreateShareChat(data))}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
{!!editLinkData && (
|
||||
<EditLinkModal
|
||||
appId={appId}
|
||||
type={'share'}
|
||||
defaultData={editLinkData}
|
||||
onCreate={(id) => {
|
||||
const url = `${location.origin}/chat/share?shareId=${id}`;
|
||||
copyData(url, '创建成功。已复制分享地址,可直接分享使用');
|
||||
refetchShareChatList();
|
||||
setEditLinkData(undefined);
|
||||
}}
|
||||
onEdit={() => {
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common.Update Successful')
|
||||
});
|
||||
refetchShareChatList();
|
||||
setEditLinkData(undefined);
|
||||
}}
|
||||
onClose={() => setEditLinkData(undefined)}
|
||||
/>
|
||||
)}
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
enum LinkTypeEnum {
|
||||
share = 'share',
|
||||
iframe = 'iframe'
|
||||
// edit link modal
|
||||
export function EditLinkModal({
|
||||
appId,
|
||||
type,
|
||||
defaultData,
|
||||
onClose,
|
||||
onCreate,
|
||||
onEdit
|
||||
}: {
|
||||
appId: string;
|
||||
type: `${OutLinkTypeEnum}`;
|
||||
defaultData: OutLinkEditType;
|
||||
onClose: () => void;
|
||||
onCreate: (id: string) => void;
|
||||
onEdit: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
|
||||
const titleMap = useRef({
|
||||
create: {
|
||||
[OutLinkTypeEnum.share]: t('outlink.Create Share Window'),
|
||||
[OutLinkTypeEnum.iframe]: t('outlink.Create Ifrme Window')
|
||||
},
|
||||
edit: {
|
||||
[OutLinkTypeEnum.share]: t('outlink.Edit Share Window'),
|
||||
[OutLinkTypeEnum.iframe]: t('outlink.Edit Ifrme Link')
|
||||
}
|
||||
});
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit: submitShareChat
|
||||
} = useForm({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
|
||||
const { mutate: onclickCreate, isLoading: creating } = useRequest({
|
||||
mutationFn: async (e: OutLinkEditType) =>
|
||||
createShareChat({
|
||||
...e,
|
||||
appId,
|
||||
type
|
||||
}),
|
||||
errorToast: '创建链接异常',
|
||||
onSuccess: onCreate
|
||||
});
|
||||
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
|
||||
mutationFn: (e: OutLinkEditType) => {
|
||||
console.log(e);
|
||||
return putShareChat(e);
|
||||
},
|
||||
errorToast: '更新链接异常',
|
||||
onSuccess: onEdit
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={() => {}}
|
||||
title={isEdit ? titleMap.current.edit[type] : titleMap.current.create[type]}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 90px'}>{t('Name')}:</Box>
|
||||
<Input
|
||||
placeholder={t('outlink.Link Name') || 'Link Name'}
|
||||
maxLength={20}
|
||||
{...register('name', {
|
||||
required: t('common.Name is empty') || 'Name is empty'
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
QPM:
|
||||
<MyTooltip label={t('outlink.QPM Tips' || '')}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Input
|
||||
max={1000}
|
||||
{...register('limit.QPM', {
|
||||
min: 0,
|
||||
max: 1000,
|
||||
valueAsNumber: true,
|
||||
required: t('outlink.QPM is empty') || ''
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
{t('outlink.Max credit')}:
|
||||
<MyTooltip label={t('outlink.Max credit tips' || '')}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Input
|
||||
{...register('limit.credit', {
|
||||
min: -1,
|
||||
max: 1000,
|
||||
valueAsNumber: true,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
{t('common.Expired Time')}:
|
||||
</Flex>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
defaultValue={
|
||||
defaultData.limit?.expiredTime
|
||||
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setValue('limit.expiredTime', new Date(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
{t('outlink.Response Detail')}:
|
||||
<MyTooltip label={t('outlink.Response Detail tips' || '')}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Switch {...register('responseDetail')} size={'lg'} />
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isLoading={creating || updating}
|
||||
onClick={submitShareChat((data) => (isEdit ? onclickUpdate(data) : onclickCreate(data)))}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
const OutLink = ({ appId }: { appId: string }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [linkType, setLinkType] = useState<`${LinkTypeEnum}`>(LinkTypeEnum.share);
|
||||
const [linkType, setLinkType] = useState<`${OutLinkTypeEnum}`>(OutLinkTypeEnum.share);
|
||||
|
||||
return (
|
||||
<Box pt={[1, 5]}>
|
||||
@@ -241,21 +391,21 @@ const OutLink = ({ appId }: { appId: string }) => {
|
||||
icon: 'outlink_share',
|
||||
title: '免登录窗口',
|
||||
desc: '分享链接给其他用户,无需登录即可直接进行使用',
|
||||
value: LinkTypeEnum.share
|
||||
value: OutLinkTypeEnum.share
|
||||
}
|
||||
// {
|
||||
// icon: 'outlink_iframe',
|
||||
// title: '网页嵌入',
|
||||
// desc: '嵌入到已有网页中,右下角会生成对话按键',
|
||||
// value: LinkTypeEnum.iframe
|
||||
// value: OutLinkTypeEnum.iframe
|
||||
// }
|
||||
]}
|
||||
value={linkType}
|
||||
onChange={(e) => setLinkType(e as `${LinkTypeEnum}`)}
|
||||
onChange={(e) => setLinkType(e as `${OutLinkTypeEnum}`)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{linkType === LinkTypeEnum.share && <Share appId={appId} />}
|
||||
{linkType === OutLinkTypeEnum.share && <Share appId={appId} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
IconButton
|
||||
} from '@chakra-ui/react';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useEditInfo } from '@/hooks/useEditInfo';
|
||||
import { useEditTitle } from '@/hooks/useEditTitle';
|
||||
import { useRouter } from 'next/router';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
@@ -70,7 +70,7 @@ const ChatHistorySlider = ({
|
||||
const isShare = useMemo(() => !appId || !userInfo, [appId, userInfo]);
|
||||
|
||||
// custom title edit
|
||||
const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({
|
||||
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: '自定义历史记录标题',
|
||||
placeholder: '如果设置为空,会自动跟随聊天记录。'
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { initShareChatInfo } from '@/api/chat';
|
||||
import { initShareChatInfo } from '@/api/support/outLink';
|
||||
import { Box, Flex, useDisclosure, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
@@ -64,6 +64,7 @@ const OutLink = ({ shareId, chatId }: { shareId: string; chatId: string }) => {
|
||||
status: 'finish'
|
||||
}));
|
||||
result[1].value = responseText;
|
||||
result[1].responseData = responseData;
|
||||
|
||||
/* save chat */
|
||||
saveChatResponse({
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Box, Flex, Button, Image } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { feConfigs } from '@/store/static';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
||||
const Hero = () => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { isPc, gitStar } = useGlobalStore();
|
||||
const [showVideo, setShowVide] = useState(false);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} pt={['24px', '50px']} alignItems={'center'} userSelect={'none'}>
|
||||
@@ -62,6 +61,7 @@ const Hero = () => {
|
||||
mx={['-10%', 'auto']}
|
||||
maxW={['120%', '1000px']}
|
||||
alt=""
|
||||
draggable={false}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'playFill'}
|
||||
@@ -72,13 +72,42 @@ const Hero = () => {
|
||||
top={'50%'}
|
||||
color={'#363c42b8'}
|
||||
transform={['translate(-50%,5px)', 'translate(-50%,40px)']}
|
||||
onClick={() => {
|
||||
toast({
|
||||
title: '录制中~'
|
||||
});
|
||||
}}
|
||||
onClick={() => setShowVide(true)}
|
||||
/>
|
||||
</Box>
|
||||
{showVideo && (
|
||||
<Flex
|
||||
position={'fixed'}
|
||||
zIndex={99}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
bg={'rgba(255,255,255,0.4)'}
|
||||
onClick={() => setShowVide(false)}
|
||||
>
|
||||
<Box
|
||||
w={['100vw', '50%']}
|
||||
borderRadius={'lg'}
|
||||
overflow={'hidden'}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<video
|
||||
style={{
|
||||
margin: 'auto'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
src={'https://otnvvf-imgs.oss.laf.run/fastgpt.mp4'}
|
||||
controls
|
||||
autoPlay
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import { Box, Card, IconButton, Flex, Button, Grid, Image } from '@chakra-ui/react';
|
||||
import { Box, Card, IconButton, Flex, Grid, Image } from '@chakra-ui/react';
|
||||
import type { KbDataItemType } from '@/types/plugin';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import {
|
||||
getKbDataList,
|
||||
getExportDataList,
|
||||
delOneKbDataByDataId,
|
||||
getTrainingData,
|
||||
getFileInfoById
|
||||
} from '@/api/plugins/kb';
|
||||
import { getKbDataList, delOneKbDataByDataId, getTrainingData } from '@/api/plugins/kb';
|
||||
import { getFileInfoById } from '@/api/core/dataset/file';
|
||||
import { DeleteIcon, RepeatIcon } from '@chakra-ui/icons';
|
||||
import { fileDownload } from '@/utils/file';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import Papa from 'papaparse';
|
||||
import InputModal, { FormData as InputDataType } from './InputDataModal';
|
||||
import { debounce } from 'lodash';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
@@ -24,8 +17,6 @@ import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyInput from '@/components/MyInput';
|
||||
import { fileImgs } from '@/constants/common';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { feConfigs } from '@/store/static';
|
||||
|
||||
const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
const BoxRef = useRef<HTMLDivElement>(null);
|
||||
@@ -65,22 +56,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
|
||||
const [editInputData, setEditInputData] = useState<InputDataType>();
|
||||
|
||||
const { data: { qaListLen = 0, vectorListLen = 0 } = {}, refetch: refetchTrainingData } =
|
||||
useQuery(['getModelSplitDataList', kbId], () => getTrainingData({ kbId, init: false }), {
|
||||
onError(err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
|
||||
const refetchData = useCallback(
|
||||
(num = pageNum) => {
|
||||
getData(num);
|
||||
refetchTrainingData();
|
||||
return null;
|
||||
},
|
||||
[getData, pageNum, refetchTrainingData]
|
||||
);
|
||||
|
||||
// get first page data
|
||||
const getFirstData = useCallback(
|
||||
debounce(() => {
|
||||
@@ -90,12 +65,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
// interval get data
|
||||
useQuery(['refetchData'], () => refetchData(1), {
|
||||
refetchInterval: 5000,
|
||||
enabled: qaListLen > 0 || vectorListLen > 0
|
||||
});
|
||||
|
||||
// get file info
|
||||
const { data: fileInfo } = useQuery(['getFileInfo', fileId], () => getFileInfoById(fileId));
|
||||
const fileIcon = useMemo(
|
||||
@@ -104,31 +73,27 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
[fileInfo?.filename]
|
||||
);
|
||||
|
||||
// get al data and export csv
|
||||
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useRequest({
|
||||
mutationFn: () => getExportDataList({ kbId, fileId }),
|
||||
onSuccess(res) {
|
||||
const text = Papa.unparse({
|
||||
fields: ['question', 'answer', 'source'],
|
||||
data: res
|
||||
});
|
||||
|
||||
const filenameSplit = fileInfo?.filename?.split('.') || [];
|
||||
const filename = filenameSplit?.length <= 1 ? 'data' : filenameSplit.slice(0, -1).join('.');
|
||||
|
||||
fileDownload({
|
||||
text,
|
||||
type: 'text/csv',
|
||||
filename
|
||||
});
|
||||
},
|
||||
successToast: `导出成功,下次导出需要 ${feConfigs.exportLimitMinutes} 分钟后`,
|
||||
errorToast: '导出异常'
|
||||
});
|
||||
|
||||
return (
|
||||
<Box ref={BoxRef} position={'relative'} px={5} py={[1, 5]} h={'100%'} overflow={'overlay'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<IconButton
|
||||
mr={3}
|
||||
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
|
||||
bg={'white'}
|
||||
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
|
||||
h={'28px'}
|
||||
size={'sm'}
|
||||
borderRadius={'50%'}
|
||||
aria-label={''}
|
||||
onClick={() =>
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: 'dataset'
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Flex
|
||||
className="textEllipsis"
|
||||
flex={'1 0 0'}
|
||||
@@ -136,21 +101,9 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
fontSize={['sm', 'md']}
|
||||
alignItems={'center'}
|
||||
>
|
||||
<Image src={fileIcon} w={'16px'} mr={2} alt={''} />
|
||||
<Image src={fileIcon || '/imgs/files/file.svg'} w={'16px'} mr={2} alt={''} />
|
||||
{t(fileInfo?.filename || 'Filename')}
|
||||
</Flex>
|
||||
<Button
|
||||
mr={2}
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
borderColor={'myBlue.600'}
|
||||
color={'myBlue.600'}
|
||||
isLoading={isLoadingExport || isLoading}
|
||||
title={`${feConfigs} 分钟能导出 1 次`}
|
||||
onClick={onclickExport}
|
||||
>
|
||||
{t('dataset.Export')}
|
||||
</Button>
|
||||
<Box>
|
||||
<MyTooltip label={'刷新'}>
|
||||
<IconButton
|
||||
@@ -172,15 +125,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
<Box as={'span'} fontSize={['md', 'lg']}>
|
||||
{total}组
|
||||
</Box>
|
||||
<Box as={'span'}>
|
||||
{(qaListLen > 0 || vectorListLen > 0) && (
|
||||
<>
|
||||
({qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
||||
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
||||
请耐心等待... )
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={1} mr={1} />
|
||||
<MyInput
|
||||
@@ -262,7 +206,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await delOneKbDataByDataId(item.id);
|
||||
refetchData(pageNum);
|
||||
getData(pageNum);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: getErrText(error),
|
||||
@@ -297,7 +241,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
||||
kbId={kbId}
|
||||
defaultValues={editInputData}
|
||||
onClose={() => setEditInputData(undefined)}
|
||||
onSuccess={() => refetchData()}
|
||||
onSuccess={() => getData(pageNum)}
|
||||
/>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
|
||||
@@ -9,11 +9,15 @@ import {
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
Image
|
||||
Image,
|
||||
MenuButton,
|
||||
Menu,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { getKbFiles, deleteKbFileById } from '@/api/plugins/kb';
|
||||
import { getTrainingData } from '@/api/plugins/kb';
|
||||
import { getDatasetFiles, delDatasetFileById, updateDatasetFile } from '@/api/core/dataset/file';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { debounce } from 'lodash';
|
||||
import { formatFileSize } from '@/utils/tools';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
@@ -24,37 +28,58 @@ import dayjs from 'dayjs';
|
||||
import { fileImgs } from '@/constants/common';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { FileStatusEnum } from '@/constants/kb';
|
||||
import { FileStatusEnum, OtherFileId } from '@/constants/kb';
|
||||
import { useRouter } from 'next/router';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { KbFileItemType } from '@/types/plugin';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyMenu from '@/components/MyMenu';
|
||||
import { useEditTitle } from '@/hooks/useEditTitle';
|
||||
|
||||
const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
const BoxRef = useRef<HTMLDivElement>(null);
|
||||
const lastSearch = useRef('');
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { Loading } = useLoading();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('kb.Confirm to delete the file')
|
||||
});
|
||||
|
||||
const {
|
||||
data: files = [],
|
||||
refetch,
|
||||
isInitialLoading
|
||||
} = useQuery(['getFiles', kbId], () => getKbFiles({ kbId, searchText }), {
|
||||
refetchInterval: 6000,
|
||||
refetchOnWindowFocus: true
|
||||
data: files,
|
||||
Pagination,
|
||||
total,
|
||||
getData,
|
||||
isLoading,
|
||||
pageNum,
|
||||
pageSize
|
||||
} = usePagination<KbFileItemType>({
|
||||
api: getDatasetFiles,
|
||||
pageSize: 20,
|
||||
params: {
|
||||
kbId,
|
||||
searchText
|
||||
},
|
||||
onChange() {
|
||||
if (BoxRef.current) {
|
||||
BoxRef.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// change search
|
||||
const debounceRefetch = useCallback(
|
||||
debounce(() => {
|
||||
refetch();
|
||||
getData(1);
|
||||
lastSearch.current = searchText;
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
// add file icon
|
||||
const formatFiles = useMemo(
|
||||
() =>
|
||||
files.map((file) => ({
|
||||
@@ -63,23 +88,42 @@ const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
})),
|
||||
[files]
|
||||
);
|
||||
const totalDataLength = useMemo(
|
||||
() => files.reduce((sum, item) => sum + item.chunkLength, 0),
|
||||
[files]
|
||||
);
|
||||
|
||||
const { mutate: onDeleteFile, isLoading } = useRequest({
|
||||
mutationFn: (fileId: string) =>
|
||||
deleteKbFileById({
|
||||
const { mutate: onDeleteFile } = useRequest({
|
||||
mutationFn: (fileId: string) => {
|
||||
setLoading(true);
|
||||
return delDatasetFileById({
|
||||
fileId,
|
||||
kbId
|
||||
}),
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
refetch();
|
||||
getData(pageNum);
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false);
|
||||
},
|
||||
successToast: t('common.Delete Success'),
|
||||
errorToast: t('common.Delete Failed')
|
||||
});
|
||||
const { mutate: onUpdateFilename } = useRequest({
|
||||
mutationFn: (data: { id: string; name: string }) => {
|
||||
setLoading(true);
|
||||
return updateDatasetFile(data);
|
||||
},
|
||||
onSuccess() {
|
||||
getData(pageNum);
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false);
|
||||
},
|
||||
successToast: t('common.Delete Success'),
|
||||
errorToast: t('common.Delete Failed')
|
||||
});
|
||||
|
||||
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('Rename')
|
||||
});
|
||||
|
||||
const statusMap = {
|
||||
[FileStatusEnum.embedding]: {
|
||||
@@ -92,18 +136,48 @@ const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// training data
|
||||
const { data: { qaListLen = 0, vectorListLen = 0 } = {}, refetch: refetchTrainingData } =
|
||||
useQuery(['getModelSplitDataList', kbId], () => getTrainingData({ kbId, init: false }), {
|
||||
onError(err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
|
||||
useQuery(
|
||||
['refetchTrainingData', kbId],
|
||||
() => Promise.all([refetchTrainingData(), getData(pageNum)]),
|
||||
{
|
||||
refetchInterval: 8000,
|
||||
enabled: qaListLen > 0 || vectorListLen > 0
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box ref={BoxRef} position={'relative'} py={[1, 5]} h={'100%'} overflow={'overlay'}>
|
||||
<Flex justifyContent={'space-between'} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
|
||||
{t('kb.Files', { total: files.length })}
|
||||
<Box ref={BoxRef} py={[1, 5]} h={'100%'} overflow={'overlay'}>
|
||||
<Flex justifyContent={'space-between'} px={[2, 5]}>
|
||||
<Box>
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mr={2}>
|
||||
{t('kb.Files', { total })}
|
||||
</Box>
|
||||
<Box as={'span'} fontSize={'sm'}>
|
||||
{(qaListLen > 0 || vectorListLen > 0) && (
|
||||
<>
|
||||
({qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
||||
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
||||
请耐心等待... )
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Flex alignItems={'center'}>
|
||||
<MyInput
|
||||
leftIcon={
|
||||
<MyIcon name="searchLight" position={'absolute'} w={'14px'} color={'myGray.500'} />
|
||||
}
|
||||
w={['100%', '200px']}
|
||||
w={['100%', '250px']}
|
||||
size={['sm', 'md']}
|
||||
placeholder={t('common.Search') || ''}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
@@ -112,25 +186,23 @@ const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (searchText === lastSearch.current) return;
|
||||
refetch();
|
||||
getData(1);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (searchText === lastSearch.current) return;
|
||||
if (e.key === 'Enter') {
|
||||
refetch();
|
||||
getData(1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<TableContainer mt={[0, 3]}>
|
||||
<TableContainer mt={[0, 3]} position={'relative'} minH={'70vh'}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('kb.Filename')}</Th>
|
||||
<Th>
|
||||
{t('kb.Chunk Length')}({totalDataLength})
|
||||
</Th>
|
||||
<Th>{t('kb.Chunk Length')}</Th>
|
||||
<Th>{t('kb.Upload Time')}</Th>
|
||||
<Th>{t('kb.File Size')}</Th>
|
||||
<Th>{t('common.Status')}</Th>
|
||||
@@ -145,7 +217,7 @@ const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
cursor={'pointer'}
|
||||
title={'点击查看数据详情'}
|
||||
onClick={() =>
|
||||
router.push({
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
fileId: file.id,
|
||||
@@ -167,40 +239,103 @@ const FileCard = ({ kbId }: { kbId: string }) => {
|
||||
</Td>
|
||||
<Td>{dayjs(file.uploadTime).format('YYYY/MM/DD HH:mm')}</Td>
|
||||
<Td>{formatFileSize(file.size)}</Td>
|
||||
<Td
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
_before={{
|
||||
content: '""',
|
||||
w: '10px',
|
||||
h: '10px',
|
||||
mr: 2,
|
||||
borderRadius: 'lg',
|
||||
bg: statusMap[file.status].color
|
||||
}}
|
||||
>
|
||||
{statusMap[file.status].text}
|
||||
<Td>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
_before={{
|
||||
content: '""',
|
||||
w: '10px',
|
||||
h: '10px',
|
||||
mr: 2,
|
||||
borderRadius: 'lg',
|
||||
bg: statusMap[file.status].color
|
||||
}}
|
||||
>
|
||||
{statusMap[file.status].text}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td onClick={(e) => e.stopPropagation()}>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() =>
|
||||
openConfirm(() => {
|
||||
onDeleteFile(file.id);
|
||||
})()
|
||||
<MyMenu
|
||||
width={100}
|
||||
Button={
|
||||
<MenuButton
|
||||
w={'22px'}
|
||||
h={'22px'}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
color: 'myBlue.600',
|
||||
'& .icon': {
|
||||
bg: 'myGray.100'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
className="icon"
|
||||
name={'more'}
|
||||
h={'16px'}
|
||||
w={'16px'}
|
||||
px={1}
|
||||
py={1}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
</MenuButton>
|
||||
}
|
||||
menuList={[
|
||||
...(file.id !== OtherFileId
|
||||
? [
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'edit'} w={'14px'} mr={2} />
|
||||
{t('Rename')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
onOpenModal({
|
||||
defaultVal: file.filename,
|
||||
onSuccess: (newName) => {
|
||||
onUpdateFilename({
|
||||
id: file.id,
|
||||
name: newName
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
/>
|
||||
<Box>{t('common.Delete')}</Box>
|
||||
</Flex>
|
||||
),
|
||||
onClick: openConfirm(() => {
|
||||
onDeleteFile(file.id);
|
||||
})
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Loading loading={isLoading && files.length === 0} fixed={false} />
|
||||
{total > pageSize && (
|
||||
<Flex mt={2} justifyContent={'center'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
<ConfirmModal />
|
||||
<Loading loading={isInitialLoading || isLoading} />
|
||||
<EditTitleModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import FileSelect, { type FileItemType } from './FileSelect';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
import { updateDatasetFile } from '@/api/core/dataset/file';
|
||||
|
||||
const fileExtension = '.txt, .doc, .docx, .pdf, .md';
|
||||
|
||||
@@ -64,6 +65,16 @@ const ChunkImport = ({ kbId }: { kbId: string }) => {
|
||||
mutationFn: async () => {
|
||||
const chunks = files.map((file) => file.chunks).flat();
|
||||
|
||||
// mark the file is used
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
updateDatasetFile({
|
||||
id: file.id,
|
||||
datasetUsed: true
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// subsection import
|
||||
let success = 0;
|
||||
const step = 300;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import FileSelect, { type FileItemType } from './FileSelect';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useDatasetStore } from '@/store/dataset';
|
||||
import { updateDatasetFile } from '@/api/core/dataset/file';
|
||||
|
||||
const fileExtension = '.csv';
|
||||
|
||||
@@ -37,6 +38,16 @@ const CsvImport = ({ kbId }: { kbId: string }) => {
|
||||
|
||||
const { mutate: onclickUpload, isLoading: uploading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
// mark the file is used
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
updateDatasetFile({
|
||||
id: file.id,
|
||||
datasetUsed: true
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const chunks = files
|
||||
.map((file) => file.chunks)
|
||||
.flat()
|
||||
|
||||
@@ -152,7 +152,7 @@ const FileSelect = ({
|
||||
throw new Error('csv 文件格式有误,请确保 question 和 answer 两列');
|
||||
}
|
||||
const fileItem: FileItemType = {
|
||||
id: nanoid(),
|
||||
id: filesId[0],
|
||||
filename: file.name,
|
||||
icon,
|
||||
tokens: 0,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import FileSelect, { type FileItemType } from './FileSelect';
|
||||
import { useRouter } from 'next/router';
|
||||
import { updateDatasetFile } from '@/api/core/dataset/file';
|
||||
|
||||
const fileExtension = '.txt, .doc, .docx, .pdf, .md';
|
||||
|
||||
@@ -40,7 +41,11 @@ const QAImport = ({ kbId }: { kbId: string }) => {
|
||||
|
||||
// price count
|
||||
const price = useMemo(() => {
|
||||
return formatPrice(files.reduce((sum, file) => sum + file.tokens, 0) * unitPrice * 1.3);
|
||||
const filesToken = files.reduce((sum, file) => sum + file.tokens, 0);
|
||||
const promptTokens = files.reduce((sum, file) => sum + file.chunks.length, 0) * 139;
|
||||
const totalToken = (filesToken + promptTokens) * 1.8;
|
||||
|
||||
return formatPrice(totalToken * unitPrice);
|
||||
}, [files, unitPrice]);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
@@ -51,6 +56,16 @@ const QAImport = ({ kbId }: { kbId: string }) => {
|
||||
mutationFn: async () => {
|
||||
const chunks = files.map((file) => file.chunks).flat();
|
||||
|
||||
// mark the file is used
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
updateDatasetFile({
|
||||
id: file.id,
|
||||
datasetUsed: true
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// subsection import
|
||||
let success = 0;
|
||||
const step = 200;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
|
||||
import { Box, Flex, Button, Textarea, IconButton, BoxProps } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { insertData2Kb, putKbDataById, delOneKbDataByDataId } from '@/api/plugins/kb';
|
||||
import { getFileViewUrl } from '@/api/system';
|
||||
import { getFileViewUrl } from '@/api/support/file';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
@@ -17,7 +17,8 @@ import Avatar from '@/components/Avatar';
|
||||
import Info from './components/Info';
|
||||
import { serviceSideProps } from '@/utils/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { delEmptyFiles, getTrainingQueueLen } from '@/api/plugins/kb';
|
||||
import { getTrainingQueueLen } from '@/api/plugins/kb';
|
||||
import { delDatasetEmptyFiles } from '@/api/core/dataset/file';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { feConfigs } from '@/store/static';
|
||||
@@ -96,7 +97,7 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
delEmptyFiles(kbId);
|
||||
delDatasetEmptyFiles(kbId);
|
||||
} catch (error) {}
|
||||
};
|
||||
}, [kbId]);
|
||||
|
||||
181
client/src/pages/kb/list/component/MoveModal.tsx
Normal file
181
client/src/pages/kb/list/component/MoveModal.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
useTheme,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import { getKbPaths } from '@/api/plugins/kb';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getKbList, putKbById } from '@/api/plugins/kb';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
|
||||
const MoveModal = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
moveDataId
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
moveDataId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const [parentId, setParentId] = useState<string>('');
|
||||
|
||||
const { data } = useQuery(['getKbList', parentId], () => {
|
||||
return Promise.all([getKbList({ parentId, type: 'folder' }), getKbPaths(parentId)]);
|
||||
});
|
||||
const paths = useMemo(
|
||||
() => [
|
||||
{
|
||||
parentId: '',
|
||||
parentName: t('kb.My Dataset')
|
||||
},
|
||||
...(data?.[1] || [])
|
||||
],
|
||||
[data, t]
|
||||
);
|
||||
const folderList = useMemo(
|
||||
() => (data?.[0] || []).filter((item) => item._id !== moveDataId),
|
||||
[moveDataId, data]
|
||||
);
|
||||
|
||||
const { mutate, isLoading } = useRequest({
|
||||
mutationFn: () => putKbById({ id: moveDataId, parentId }),
|
||||
onSuccess,
|
||||
errorToast: t('kb.Move Failed')
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} maxW={['90vw', '800px']} w={'800px'} onClose={onClose}>
|
||||
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
|
||||
<ModalHeader>
|
||||
{!!parentId ? (
|
||||
<Flex flex={1} userSelect={'none'} fontSize={['sm', 'lg']} fontWeight={'normal'}>
|
||||
{paths.map((item, i) => (
|
||||
<Flex key={item.parentId} mr={2} alignItems={'center'}>
|
||||
<Box
|
||||
borderRadius={'md'}
|
||||
{...(i === paths.length - 1
|
||||
? {
|
||||
cursor: 'default'
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
color: 'myBlue.600'
|
||||
},
|
||||
onClick: () => {
|
||||
setParentId(item.parentId);
|
||||
}
|
||||
})}
|
||||
>
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== paths.length - 1 && (
|
||||
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['18px', '24px']} />
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
) : (
|
||||
<Box>我的知识库</Box>
|
||||
)}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
flex={['1 0 0', '0 0 auto']}
|
||||
maxH={'80vh'}
|
||||
overflowY={'auto'}
|
||||
display={'grid'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Grid
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
|
||||
gridGap={3}
|
||||
>
|
||||
{folderList.map((item) =>
|
||||
(() => {
|
||||
return (
|
||||
<MyTooltip
|
||||
key={item._id}
|
||||
label={
|
||||
item.type === KbTypeEnum.dataset
|
||||
? t('kb.Select Dataset')
|
||||
: t('kb.Select Folder')
|
||||
}
|
||||
>
|
||||
<Card
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
onClick={() => {
|
||||
setParentId(item._id);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
|
||||
<Box
|
||||
className="textEllipsis"
|
||||
ml={3}
|
||||
fontWeight={'bold'}
|
||||
fontSize={['md', 'lg', 'xl']}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
|
||||
{item.type === KbTypeEnum.folder ? (
|
||||
<Box color={'myGray.500'}>{t('Folder')}</Box>
|
||||
) : (
|
||||
<>
|
||||
<MyIcon mr={1} name="kbTest" w={'12px'} />
|
||||
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
</MyTooltip>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</Grid>
|
||||
{folderList.length === 0 && (
|
||||
<Flex mt={5} flexDirection={'column'} alignItems={'center'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t('kb.No Folder')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button isLoading={isLoading} onClick={mutate}>
|
||||
{t('kb.Confirm move the folder')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoveModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
useTheme,
|
||||
useDisclosure,
|
||||
Card,
|
||||
IconButton,
|
||||
MenuButton,
|
||||
Image
|
||||
} from '@chakra-ui/react';
|
||||
@@ -16,8 +15,7 @@ import PageContainer from '@/components/PageContainer';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { delKbById, getKbPaths } from '@/api/plugins/kb';
|
||||
import { delKbById, getExportDataList, getKbPaths, putKbById } from '@/api/plugins/kb';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
@@ -28,16 +26,20 @@ import Tag from '@/components/Tag';
|
||||
import MyMenu from '@/components/MyMenu';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useEditTitle } from '@/hooks/useEditTitle';
|
||||
import Papa from 'papaparse';
|
||||
import { fileDownload } from '@/utils/file';
|
||||
import { feConfigs } from '@/store/static';
|
||||
|
||||
const CreateModal = dynamic(() => import('./component/CreateModal'), { ssr: false });
|
||||
const EditFolderModal = dynamic(() => import('./component/EditFolderModal'), { ssr: false });
|
||||
const MoveModal = dynamic(() => import('./component/MoveModal'), { ssr: false });
|
||||
|
||||
const Kb = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { parentId } = router.query as { parentId: string };
|
||||
const { toast } = useToast();
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
const DeleteTipsMap = useRef({
|
||||
@@ -49,7 +51,13 @@ const Kb = () => {
|
||||
title: t('common.Delete Warning'),
|
||||
content: ''
|
||||
});
|
||||
const { myKbList, loadKbList, setKbList } = useDatasetStore();
|
||||
const { myKbList, loadKbList, setKbList, updateDataset } = useDatasetStore();
|
||||
const { onOpenModal: onOpenTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('Rename')
|
||||
});
|
||||
const [moveDataId, setMoveDataId] = useState<string>();
|
||||
const [dragStartId, setDragStartId] = useState<string>();
|
||||
const [dragTargetId, setDragTargetId] = useState<string>();
|
||||
|
||||
const {
|
||||
isOpen: isOpenCreateModal,
|
||||
@@ -78,7 +86,32 @@ const Kb = () => {
|
||||
errorToast: t('kb.Delete Dataset Error')
|
||||
});
|
||||
|
||||
const { data, refetch } = useQuery(['loadKbList', parentId], () => {
|
||||
// export dataset to csv
|
||||
const { mutate: onclickExport } = useRequest({
|
||||
mutationFn: (kbId: string) => {
|
||||
setLoading(true);
|
||||
return getExportDataList({ kbId });
|
||||
},
|
||||
onSuccess(res) {
|
||||
const text = Papa.unparse({
|
||||
fields: ['question', 'answer', 'source'],
|
||||
data: res
|
||||
});
|
||||
|
||||
fileDownload({
|
||||
text,
|
||||
type: 'text/csv',
|
||||
filename: 'dataset.csv'
|
||||
});
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false);
|
||||
},
|
||||
successToast: `导出成功,下次导出需要 ${feConfigs?.limit?.exportLimitMinutes} 分钟后`,
|
||||
errorToast: '导出异常'
|
||||
});
|
||||
|
||||
const { data, refetch } = useQuery(['loadDataset', parentId], () => {
|
||||
return Promise.all([loadKbList(parentId), getKbPaths(parentId)]);
|
||||
});
|
||||
|
||||
@@ -102,8 +135,8 @@ const Kb = () => {
|
||||
{paths.map((item, i) => (
|
||||
<Flex key={item.parentId} mr={2} alignItems={'center'}>
|
||||
<Box
|
||||
fontSize={'lg'}
|
||||
px={2}
|
||||
fontSize={['sm', 'lg']}
|
||||
px={[0, 2]}
|
||||
py={1}
|
||||
borderRadius={'md'}
|
||||
{...(i === paths.length - 1
|
||||
@@ -126,7 +159,9 @@ const Kb = () => {
|
||||
>
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== paths.length - 1 && <MyIcon name={'rightArrowLight'} color={'myGray.500'} />}
|
||||
{i !== paths.length - 1 && (
|
||||
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['18px', '24px']} />
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
@@ -184,6 +219,7 @@ const Kb = () => {
|
||||
p={5}
|
||||
gridTemplateColumns={['1fr', 'repeat(3,1fr)', 'repeat(4,1fr)', 'repeat(5,1fr)']}
|
||||
gridGap={5}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{myKbList.map((kb) => (
|
||||
<Card
|
||||
@@ -196,8 +232,36 @@ const Kb = () => {
|
||||
h={'130px'}
|
||||
border={theme.borders.md}
|
||||
boxShadow={'none'}
|
||||
userSelect={'none'}
|
||||
position={'relative'}
|
||||
data-drag-id={kb.type === KbTypeEnum.folder ? kb._id : undefined}
|
||||
borderColor={dragTargetId === kb._id ? 'myBlue.600' : ''}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
setDragStartId(kb._id);
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
const targetId = e.currentTarget.getAttribute('data-drag-id');
|
||||
if (!targetId) return;
|
||||
KbTypeEnum.folder && setDragTargetId(targetId);
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
setDragTargetId(undefined);
|
||||
}}
|
||||
onDrop={async (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragTargetId || !dragStartId || dragTargetId === dragStartId) return;
|
||||
// update parentId
|
||||
try {
|
||||
await putKbById({
|
||||
id: dragStartId,
|
||||
parentId: dragTargetId
|
||||
});
|
||||
refetch();
|
||||
} catch (error) {}
|
||||
setDragTargetId(undefined);
|
||||
}}
|
||||
_hover={{
|
||||
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)',
|
||||
borderColor: 'transparent',
|
||||
@@ -223,33 +287,94 @@ const Kb = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyMenu
|
||||
offset={[-30, 10]}
|
||||
width={120}
|
||||
Button={
|
||||
<MenuButton
|
||||
position={'absolute'}
|
||||
top={3}
|
||||
right={3}
|
||||
w={'22px'}
|
||||
h={'22px'}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
color: 'myBlue.600',
|
||||
'& .icon': {
|
||||
bg: 'myGray.100'
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
className="icon"
|
||||
name={'more'}
|
||||
h={'16px'}
|
||||
w={'16px'}
|
||||
px={1}
|
||||
py={1}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
</MenuButton>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'edit'} w={'14px'} mr={2} />
|
||||
{t('Rename')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
onOpenTitleModal({
|
||||
defaultVal: kb.name,
|
||||
onSuccess: (val) => {
|
||||
if (val === kb.name || !val) return;
|
||||
updateDataset({ id: kb._id, name: val });
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'moveLight'} w={'14px'} mr={2} />
|
||||
{t('Move')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () => setMoveDataId(kb._id)
|
||||
},
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'export'} w={'14px'} mr={2} />
|
||||
{t('Export')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () => onclickExport(kb._id)
|
||||
},
|
||||
{
|
||||
child: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'delete'} w={'14px'} mr={2} />
|
||||
{t('common.Delete')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () => {
|
||||
openConfirm(
|
||||
() => onclickDelKb(kb._id),
|
||||
undefined,
|
||||
DeleteTipsMap.current[kb.type]
|
||||
)();
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={kb.avatar} borderRadius={'lg'} w={'28px'} />
|
||||
<Box ml={3}>{kb.name}</Box>
|
||||
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
display={['', 'none']}
|
||||
_hover={{
|
||||
bg: 'red.100'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openConfirm(
|
||||
() => onclickDelKb(kb._id),
|
||||
undefined,
|
||||
DeleteTipsMap.current[kb.type]
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box flex={'1 0 0'} overflow={'hidden'} pt={2}>
|
||||
<Flex>
|
||||
@@ -282,6 +407,7 @@ const Kb = () => {
|
||||
</Flex>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
<EditTitleModal />
|
||||
{isOpenCreateModal && <CreateModal onClose={onCloseCreateModal} parentId={parentId} />}
|
||||
{!!editFolderData && (
|
||||
<EditFolderModal
|
||||
@@ -291,6 +417,16 @@ const Kb = () => {
|
||||
{...editFolderData}
|
||||
/>
|
||||
)}
|
||||
{!!moveDataId && (
|
||||
<MoveModal
|
||||
moveDataId={moveDataId}
|
||||
onClose={() => setMoveDataId('')}
|
||||
onSuccess={() => {
|
||||
refetch();
|
||||
setMoveDataId('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useState, Dispatch, useCallback } from 'react';
|
||||
import { FormControl, Flex, Input, Button, FormErrorMessage, Box } from '@chakra-ui/react';
|
||||
import React, { useState, Dispatch, useCallback, useRef } from 'react';
|
||||
import { FormControl, Flex, Input, Button, FormErrorMessage, Box, Link } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PageTypeEnum } from '@/constants/user';
|
||||
import { OAuthEnum, PageTypeEnum } from '@/constants/user';
|
||||
import { postLogin } from '@/api/user';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { feConfigs } from '@/store/static';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8);
|
||||
|
||||
interface Props {
|
||||
setPageType: Dispatch<`${PageTypeEnum}`>;
|
||||
@@ -58,18 +60,29 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
[loginSuccess, toast]
|
||||
);
|
||||
|
||||
const onclickGit = useCallback(() => {
|
||||
setLoginStore({
|
||||
provider: 'git',
|
||||
lastRoute
|
||||
});
|
||||
router.replace(
|
||||
`https://github.com/login/oauth/authorize?client_id=${
|
||||
feConfigs?.gitLoginKey
|
||||
}&redirect_uri=${`${location.origin}/login/provider`}&scope=user:email%20read:user`,
|
||||
'_self'
|
||||
);
|
||||
}, [lastRoute, setLoginStore]);
|
||||
const redirectUri = `${location.origin}/login/provider`;
|
||||
const state = useRef(nanoid());
|
||||
|
||||
const oAuthList = [
|
||||
...(feConfigs?.oauth?.github
|
||||
? [
|
||||
{
|
||||
provider: OAuthEnum.github,
|
||||
icon: 'gitFill',
|
||||
redirectUrl: `https://github.com/login/oauth/authorize?client_id=${feConfigs?.oauth?.github}&redirect_uri=${redirectUri}&state=${state.current}&scope=user:email%20read:user`
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(feConfigs?.oauth?.google
|
||||
? [
|
||||
{
|
||||
provider: OAuthEnum.google,
|
||||
icon: 'googleFill',
|
||||
redirectUrl: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${feConfigs?.oauth?.google}&redirect_uri=${redirectUri}&state=${state.current}&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20openid&include_granted_scopes=true`
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -107,28 +120,43 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{feConfigs?.show_register && (
|
||||
<Flex align={'center'} justifyContent={'space-between'} mt={3} color={'myBlue.600'}>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('forgetPassword')}
|
||||
fontSize="sm"
|
||||
>
|
||||
忘记密码?
|
||||
</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('register')}
|
||||
fontSize="sm"
|
||||
>
|
||||
注册账号
|
||||
</Box>
|
||||
</Flex>
|
||||
<>
|
||||
<Flex align={'center'} justifyContent={'space-between'} mt={3} color={'myBlue.600'}>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('forgetPassword')}
|
||||
fontSize="sm"
|
||||
>
|
||||
忘记密码?
|
||||
</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('register')}
|
||||
fontSize="sm"
|
||||
>
|
||||
注册账号
|
||||
</Box>
|
||||
</Flex>
|
||||
{feConfigs?.show_doc && (
|
||||
<Box textAlign={'center'} mt={2} fontSize={'sm'}>
|
||||
使用即代表你同意我们的{' '}
|
||||
<Link
|
||||
href="https://doc.fastgpt.run/docs/intro/#%e5%85%8d%e8%b4%a3%e5%a3%b0%e6%98%8e"
|
||||
target={'_blank'}
|
||||
color={'myBlue.600'}
|
||||
>
|
||||
免责声明
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
mt={6}
|
||||
mt={5}
|
||||
w={'100%'}
|
||||
size={['md', 'lg']}
|
||||
colorScheme="blue"
|
||||
@@ -138,14 +166,24 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
</Button>
|
||||
{feConfigs?.show_register && (
|
||||
<>
|
||||
<Flex mt={10} justifyContent={'center'} alignItems={'center'}>
|
||||
<MyIcon
|
||||
name="gitFill"
|
||||
w={'34px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.800'}
|
||||
onClick={onclickGit}
|
||||
/>
|
||||
<Flex mt={10} justifyContent={'space-around'} alignItems={'center'}>
|
||||
{oAuthList.map((item) => (
|
||||
<MyIcon
|
||||
key={item.provider}
|
||||
name={item.icon as any}
|
||||
w={'34px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.800'}
|
||||
onClick={() => {
|
||||
setLoginStore({
|
||||
provider: item.provider,
|
||||
lastRoute,
|
||||
state: state.current
|
||||
});
|
||||
router.replace(item.redirectUrl, '_self');
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { ResLogin } from '@/api/response/user';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { setToken } from '@/utils/user';
|
||||
import { gitLogin } from '@/api/user';
|
||||
import { oauthLogin } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import Loading from '@/components/Loading';
|
||||
import { serviceSideProps } from '@/utils/i18n';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
|
||||
const provider = ({ code }: { code: string }) => {
|
||||
const provider = ({ code, state }: { code: string; state: string }) => {
|
||||
const { loginStore } = useGlobalStore();
|
||||
const { setLastChatId, setLastChatAppId } = useChatStore();
|
||||
const { setUserInfo } = useUserStore();
|
||||
@@ -36,45 +36,55 @@ const provider = ({ code }: { code: string }) => {
|
||||
[setLastChatId, setLastChatAppId, setUserInfo, router, loginStore?.lastRoute]
|
||||
);
|
||||
|
||||
const authCode = useCallback(async () => {
|
||||
if (!code) return;
|
||||
if (!loginStore) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await (async () => {
|
||||
if (loginStore.provider === 'git') {
|
||||
return gitLogin({
|
||||
code,
|
||||
inviterId: localStorage.getItem('inviterId') || undefined
|
||||
const authCode = useCallback(
|
||||
async (code: string) => {
|
||||
if (!loginStore) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await oauthLogin({
|
||||
type: loginStore?.provider,
|
||||
code,
|
||||
callbackUrl: `${location.origin}/login/provider`,
|
||||
inviterId: localStorage.getItem('inviterId') || undefined
|
||||
});
|
||||
if (!res) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: '登录异常'
|
||||
});
|
||||
return setTimeout(() => {
|
||||
router.replace('/login');
|
||||
}, 1000);
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
if (!res) {
|
||||
loginSuccess(res);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: '登录异常'
|
||||
title: getErrText(error, '登录异常')
|
||||
});
|
||||
return setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
router.replace('/login');
|
||||
}, 1000);
|
||||
}
|
||||
loginSuccess(res);
|
||||
} catch (error) {
|
||||
},
|
||||
[loginStore, loginSuccess, router, toast]
|
||||
);
|
||||
|
||||
useQuery(['init', code], () => {
|
||||
if (!code) return;
|
||||
if (state !== loginStore?.state) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, '登录异常')
|
||||
title: '安全校验失败'
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.replace('/login');
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
}, [code, loginStore, loginSuccess]);
|
||||
|
||||
useQuery(['init', code], () => {
|
||||
authCode();
|
||||
authCode(code);
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -85,6 +95,7 @@ export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
code: content?.query?.code,
|
||||
state: content?.query?.state,
|
||||
...(await serviceSideProps(content))
|
||||
}
|
||||
};
|
||||
|
||||
24
client/src/service/common/ipLimit/schema.ts
Normal file
24
client/src/service/common/ipLimit/schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import type { IpLimitSchemaType } from '@/types/common/ipLimit';
|
||||
|
||||
const IpLimitSchema = new Schema({
|
||||
eventId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
ip: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
account: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
lastMinute: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
}
|
||||
});
|
||||
|
||||
export const IpLimit: Model<IpLimitSchemaType> =
|
||||
models['ip_limit'] || model('ip_limit', IpLimitSchema);
|
||||
@@ -17,6 +17,9 @@ export class GridFSStorage {
|
||||
this.bucket = bucket;
|
||||
this.uid = String(uid);
|
||||
}
|
||||
Collection() {
|
||||
return mongoose.connection.db.collection(`${this.bucket}.files`);
|
||||
}
|
||||
GridFSBucket() {
|
||||
return new mongoose.mongo.GridFSBucket(mongoose.connection.db, {
|
||||
bucketName: this.bucket
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getVector } from '@/pages/api/openapi/plugin/vector';
|
||||
import { countModelPrice } from '@/service/events/pushBill';
|
||||
import type { SelectedKbType } from '@/types/plugin';
|
||||
import type { QuoteItemType } from '@/types/chat';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
|
||||
type KBSearchProps = {
|
||||
kbList: SelectedKbType;
|
||||
@@ -42,7 +42,7 @@ export async function dispatchKBSearch(props: Record<string, any>): Promise<KBSe
|
||||
const res: any = await PgClient.query(
|
||||
`BEGIN;
|
||||
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
|
||||
select kb_id,id,q,a,source,file_id from ${PgTrainingTableName} where kb_id IN (${kbList
|
||||
select kb_id,id,q,a,source,file_id from ${PgDatasetTableName} where kb_id IN (${kbList
|
||||
.map((item) => `'${item.kbId}'`)
|
||||
.join(',')}) AND vector <#> '[${vectors[0]}]' < -${similarity} order by vector <#> '[${
|
||||
vectors[0]
|
||||
|
||||
@@ -133,7 +133,7 @@ export * from './models/trainingData';
|
||||
export * from './models/openapi';
|
||||
export * from './models/promotionRecord';
|
||||
export * from './models/collection';
|
||||
export * from './models/outLink';
|
||||
export * from './models/kb';
|
||||
export * from './models/inform';
|
||||
export * from './models/image';
|
||||
export * from './support/outLink/schema';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Pool } from 'pg';
|
||||
import type { QueryResultRow } from 'pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { PgDatasetTableName } from '@/constants/plugin';
|
||||
import { addLog } from './utils/tools';
|
||||
import { DatasetItemType } from '@/types/plugin';
|
||||
|
||||
@@ -174,12 +174,12 @@ export const insertKbItem = ({
|
||||
vector: number[];
|
||||
})[];
|
||||
}) => {
|
||||
return PgClient.insert(PgTrainingTableName, {
|
||||
return PgClient.insert(PgDatasetTableName, {
|
||||
values: data.map((item) => [
|
||||
{ key: 'user_id', value: userId },
|
||||
{ key: 'kb_id', value: kbId },
|
||||
{ key: 'source', value: item.source?.slice(0, 30)?.trim() || '' },
|
||||
{ key: 'file_id', value: item.file_id },
|
||||
{ key: 'file_id', value: item.file_id || '' },
|
||||
{ key: 'q', value: item.q.replace(/'/g, '"') },
|
||||
{ key: 'a', value: item.a.replace(/'/g, '"') },
|
||||
{ key: 'vector', value: `[${item.vector}]` }
|
||||
@@ -192,7 +192,7 @@ export async function initPg() {
|
||||
await connectPg();
|
||||
await PgClient.query(`
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
CREATE TABLE IF NOT EXISTS ${PgTrainingTableName} (
|
||||
CREATE TABLE IF NOT EXISTS ${PgDatasetTableName} (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vector VECTOR(1536) NOT NULL,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
@@ -202,9 +202,9 @@ export async function initPg() {
|
||||
q TEXT NOT NULL,
|
||||
a TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS modelData_userId_index ON ${PgTrainingTableName} USING HASH (user_id);
|
||||
CREATE INDEX IF NOT EXISTS modelData_kbId_index ON ${PgTrainingTableName} USING HASH (kb_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_data_md5_q_a_user_id_kb_id ON ${PgTrainingTableName} (md5(q), md5(a), user_id, kb_id);
|
||||
CREATE INDEX IF NOT EXISTS modelData_userId_index ON ${PgDatasetTableName} USING HASH (user_id);
|
||||
CREATE INDEX IF NOT EXISTS modelData_kbId_index ON ${PgDatasetTableName} USING HASH (kb_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_data_md5_q_a_user_id_kb_id ON ${PgDatasetTableName} (md5(q), md5(a), user_id, kb_id);
|
||||
`);
|
||||
console.log('init pg successful');
|
||||
} catch (error) {
|
||||
|
||||
79
client/src/service/support/outLink/auth.ts
Normal file
79
client/src/service/support/outLink/auth.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { PRICE_SCALE } from '@/constants/common';
|
||||
import { IpLimit } from '@/service/common/ipLimit/schema';
|
||||
import { authBalanceByUid, AuthUserTypeEnum } from '@/service/utils/auth';
|
||||
import { OutLinkSchema } from '@/types/support/outLink';
|
||||
import { OutLink } from './schema';
|
||||
|
||||
export async function authOutLinkChat({ shareId, ip }: { shareId: string; ip?: string | null }) {
|
||||
// get outLink
|
||||
const outLink = await OutLink.findOne({
|
||||
shareId
|
||||
});
|
||||
|
||||
if (!outLink) {
|
||||
return Promise.reject('分享链接无效');
|
||||
}
|
||||
|
||||
const uid = String(outLink.userId);
|
||||
|
||||
// authBalance
|
||||
const user = await authBalanceByUid(uid);
|
||||
|
||||
// limit auth
|
||||
await authOutLinkLimit({ outLink, ip });
|
||||
|
||||
return {
|
||||
user,
|
||||
userId: String(outLink.userId),
|
||||
appId: String(outLink.appId),
|
||||
authType: AuthUserTypeEnum.token,
|
||||
responseDetail: outLink.responseDetail
|
||||
};
|
||||
}
|
||||
|
||||
export async function authOutLinkLimit({
|
||||
outLink,
|
||||
ip
|
||||
}: {
|
||||
outLink: OutLinkSchema;
|
||||
ip?: string | null;
|
||||
}) {
|
||||
if (!ip || !outLink.limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (outLink.limit.expiredTime && outLink.limit.expiredTime.getTime() < Date.now()) {
|
||||
return Promise.reject('分享链接已过期');
|
||||
}
|
||||
|
||||
if (outLink.limit.credit > -1 && outLink.total > outLink.limit.credit * PRICE_SCALE) {
|
||||
return Promise.reject('链接超出使用限制');
|
||||
}
|
||||
|
||||
const ipLimit = await IpLimit.findOne({ ip, eventId: outLink._id });
|
||||
|
||||
try {
|
||||
if (!ipLimit) {
|
||||
await IpLimit.create({
|
||||
eventId: outLink._id,
|
||||
ip,
|
||||
account: outLink.limit.QPM - 1
|
||||
});
|
||||
return;
|
||||
}
|
||||
// over one minute
|
||||
const diffTime = Date.now() - ipLimit.lastMinute.getTime();
|
||||
if (diffTime >= 60 * 1000) {
|
||||
ipLimit.account = outLink.limit.QPM - 1;
|
||||
ipLimit.lastMinute = new Date();
|
||||
return await ipLimit.save();
|
||||
}
|
||||
if (ipLimit.account <= 0) {
|
||||
return Promise.reject(
|
||||
`每分钟仅能请求 ${outLink.limit.QPM} 次, ${60 - Math.round(diffTime / 1000)}s 后重试~`
|
||||
);
|
||||
}
|
||||
ipLimit.account = ipLimit.account - 1;
|
||||
await ipLimit.save();
|
||||
} catch (error) {}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { OutLinkSchema as SchmaType } from '@/types/mongoSchema';
|
||||
import { OutLinkSchema as SchemaType } from '@/types/support/outLink';
|
||||
import { OutLinkTypeEnum } from '@/constants/chat';
|
||||
|
||||
const OutLinkSchema = new Schema({
|
||||
@@ -26,12 +26,30 @@ const OutLinkSchema = new Schema({
|
||||
required: true
|
||||
},
|
||||
total: {
|
||||
// total amount
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
lastTime: {
|
||||
type: Date
|
||||
},
|
||||
responseDetail: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
limit: {
|
||||
expiredTime: {
|
||||
type: Date
|
||||
},
|
||||
QPM: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
credit: {
|
||||
type: Number,
|
||||
default: -1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const OutLink: Model<SchmaType> = models['outlinks'] || model('outlinks', OutLinkSchema);
|
||||
export const OutLink: Model<SchemaType> = models['outlinks'] || model('outlinks', OutLinkSchema);
|
||||
@@ -208,24 +208,3 @@ export const authKb = async ({ kbId, userId }: { kbId: string; userId: string })
|
||||
}
|
||||
return Promise.reject(ERROR_ENUM.unAuthKb);
|
||||
};
|
||||
|
||||
export const authShareChat = async ({ shareId }: { shareId: string }) => {
|
||||
// get shareChat
|
||||
const shareChat = await OutLink.findOne({ shareId });
|
||||
|
||||
if (!shareChat) {
|
||||
return Promise.reject('分享链接已失效');
|
||||
}
|
||||
|
||||
const uid = String(shareChat.userId);
|
||||
|
||||
// authBalance
|
||||
const user = await authBalanceByUid(uid);
|
||||
|
||||
return {
|
||||
user,
|
||||
userId: String(shareChat.userId),
|
||||
appId: String(shareChat.appId),
|
||||
authType: AuthUserTypeEnum.token
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,8 +3,9 @@ import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { type KbTestItemType } from '@/types/plugin';
|
||||
import type { KbItemType, KbListItemType } from '@/types/plugin';
|
||||
import { getKbList, getKbById, getAllDataset } from '@/api/plugins/kb';
|
||||
import { getKbList, getKbById, getAllDataset, putKbById } from '@/api/plugins/kb';
|
||||
import { defaultKbDetail } from '@/constants/kb';
|
||||
import { KbUpdateParams } from '@/api/request/kb';
|
||||
|
||||
type State = {
|
||||
datasets: KbListItemType[];
|
||||
@@ -14,6 +15,7 @@ type State = {
|
||||
setKbList(val: KbListItemType[]): void;
|
||||
kbDetail: KbItemType;
|
||||
getKbDetail: (id: string, init?: boolean) => Promise<KbItemType>;
|
||||
updateDataset: (data: KbUpdateParams) => Promise<any>;
|
||||
|
||||
kbTestList: KbTestItemType[];
|
||||
pushKbTestItem: (data: KbTestItemType) => void;
|
||||
@@ -34,8 +36,8 @@ export const useDatasetStore = create<State>()(
|
||||
return res;
|
||||
},
|
||||
myKbList: [],
|
||||
async loadKbList(parentId) {
|
||||
const res = await getKbList(parentId);
|
||||
async loadKbList(parentId = '') {
|
||||
const res = await getKbList({ parentId });
|
||||
set((state) => {
|
||||
state.myKbList = res;
|
||||
});
|
||||
@@ -58,6 +60,28 @@ export const useDatasetStore = create<State>()(
|
||||
|
||||
return data;
|
||||
},
|
||||
async updateDataset(data) {
|
||||
if (get().kbDetail._id === data.id) {
|
||||
set((state) => {
|
||||
state.kbDetail = {
|
||||
...state.kbDetail,
|
||||
...data
|
||||
};
|
||||
});
|
||||
}
|
||||
set((state) => {
|
||||
state.myKbList = state.myKbList = state.myKbList.map((item) =>
|
||||
item._id === data.id
|
||||
? {
|
||||
...item,
|
||||
...data,
|
||||
tags: data.tags?.split(' ') || []
|
||||
}
|
||||
: item
|
||||
);
|
||||
});
|
||||
await putKbById(data);
|
||||
},
|
||||
kbTestList: [],
|
||||
pushKbTestItem(data) {
|
||||
set((state) => {
|
||||
|
||||
@@ -2,8 +2,9 @@ import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import axios from 'axios';
|
||||
import { OAuthEnum } from '@/constants/user';
|
||||
|
||||
type LoginStoreType = { provider: 'git'; lastRoute: string };
|
||||
type LoginStoreType = { provider: `${OAuthEnum}`; lastRoute: string; state: string };
|
||||
|
||||
type State = {
|
||||
lastRoute: string;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getInitData } from '@/api/system';
|
||||
import { delay } from '@/utils/tools';
|
||||
import { FeConfigsType } from '@/types';
|
||||
|
||||
export let systemVersion = '0.0.0';
|
||||
export let chatModelList: ChatModelItemType[] = [];
|
||||
export let qaModel: QAModelItemType = {
|
||||
model: 'gpt-3.5-turbo-16k',
|
||||
@@ -28,6 +29,7 @@ export const clientInitData = async (): Promise<InitDateResponse> => {
|
||||
qaModel = res.qaModel;
|
||||
vectorModelList = res.vectorModels;
|
||||
feConfigs = res.feConfigs;
|
||||
systemVersion = res.systemVersion;
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
|
||||
4
client/src/types/app.d.ts
vendored
4
client/src/types/app.d.ts
vendored
@@ -38,10 +38,6 @@ export interface ShareAppItem {
|
||||
isCollection: boolean;
|
||||
}
|
||||
|
||||
export type ShareChatEditType = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/* agent */
|
||||
/* question classify */
|
||||
export type ClassifyQuestionAgentItemType = {
|
||||
|
||||
7
client/src/types/common/ipLimit.d.ts
vendored
Normal file
7
client/src/types/common/ipLimit.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export type IpLimitSchemaType = {
|
||||
_id: string;
|
||||
eventId: string;
|
||||
ip: string;
|
||||
account: number;
|
||||
lastMinute: Date;
|
||||
};
|
||||
10
client/src/types/index.d.ts
vendored
10
client/src/types/index.d.ts
vendored
@@ -27,8 +27,13 @@ export type FeConfigsType = {
|
||||
authorText?: string;
|
||||
beianText?: string;
|
||||
googleClientVerKey?: string;
|
||||
gitLoginKey?: string;
|
||||
exportLimitMinutes?: number;
|
||||
oauth?: {
|
||||
github?: string;
|
||||
google?: string;
|
||||
};
|
||||
limit?: {
|
||||
exportLimitMinutes?: number;
|
||||
};
|
||||
scripts?: { [key: string]: string }[];
|
||||
};
|
||||
export type SystemEnvType = {
|
||||
@@ -56,6 +61,7 @@ declare global {
|
||||
var chatModels: ChatModelItemType[];
|
||||
var qaModel: QAModelItemType;
|
||||
var vectorModels: VectorModelItemType[];
|
||||
var systemVersion: string;
|
||||
|
||||
interface Window {
|
||||
['pdfjs-dist/build/pdf']: any;
|
||||
|
||||
13
client/src/types/mongoSchema.d.ts
vendored
13
client/src/types/mongoSchema.d.ts
vendored
@@ -4,7 +4,7 @@ import type { DataType } from './data';
|
||||
import { BillSourceEnum, InformTypeEnum } from '@/constants/user';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import type { AppModuleItemType } from './app';
|
||||
import { ChatSourceEnum, OutLinkTypeEnum } from '@/constants/chat';
|
||||
import { ChatSourceEnum } from '@/constants/chat';
|
||||
import { AppTypeEnum } from '@/constants/app';
|
||||
import { KbTypeEnum } from '@/constants/kb';
|
||||
|
||||
@@ -156,17 +156,6 @@ export interface PromotionRecordSchema {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface OutLinkSchema {
|
||||
_id: string;
|
||||
shareId: string;
|
||||
userId: string;
|
||||
appId: string;
|
||||
name: string;
|
||||
total: number;
|
||||
lastTime: Date;
|
||||
type: `${OutLinkTypeEnum}`;
|
||||
}
|
||||
|
||||
export type kbSchema = {
|
||||
_id: string;
|
||||
userId: string;
|
||||
|
||||
25
client/src/types/support/outLink.d.ts
vendored
Normal file
25
client/src/types/support/outLink.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import { OutLinkTypeEnum } from '@/constants/chat';
|
||||
|
||||
export interface OutLinkSchema {
|
||||
_id: string;
|
||||
shareId: string;
|
||||
userId: string;
|
||||
appId: string;
|
||||
name: string;
|
||||
total: number;
|
||||
lastTime: Date;
|
||||
type: `${OutLinkTypeEnum}`;
|
||||
responseDetail: boolean;
|
||||
limit?: {
|
||||
expiredTime?: Date;
|
||||
QPM: number;
|
||||
credit: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type OutLinkEditType = {
|
||||
_id?: string;
|
||||
name: string;
|
||||
responseDetail: OutLinkSchema['responseDetail'];
|
||||
limit: OutLinkSchema['limit'];
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import mammoth from 'mammoth';
|
||||
import Papa from 'papaparse';
|
||||
import { getOpenAiEncMap } from './plugin/openai';
|
||||
import { getErrText } from './tools';
|
||||
import { uploadImg, postUploadFiles } from '@/api/system';
|
||||
import { uploadImg, postUploadFiles } from '@/api/support/file';
|
||||
|
||||
/**
|
||||
* upload file to mongo gridfs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.docs-content .main-content img, .docs-content .main-content svg {
|
||||
.docs-content .main-content img, .docs-content .main-content svg:not(.gitinfo svg) {
|
||||
max-width: 80% !important;
|
||||
height: auto;
|
||||
display: block !important;
|
||||
@@ -21,4 +21,9 @@ div.code-toolbar {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
BIN
docSite/assets/imgs/fastgpt-price.png
Normal file
BIN
docSite/assets/imgs/fastgpt-price.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -3,8 +3,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": [
|
||||
"../../../../../.cache/hugo_cache/modules/filecache/modules/pkg/mod/github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2@v2.21100.20000/package/dist/cjs/popper.js/*",
|
||||
"../../../../../.cache/hugo_cache/modules/filecache/modules/pkg/mod/github.com/twbs/bootstrap@v5.3.0+incompatible/js/*"
|
||||
"../../../../../Library/Caches/hugo_cache/modules/filecache/modules/pkg/mod/github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2@v2.21100.20000/package/dist/cjs/popper.js/*",
|
||||
"../../../../../Library/Caches/hugo_cache/modules/filecache/modules/pkg/mod/github.com/twbs/bootstrap@v5.3.0+incompatible/js/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
84
docSite/content/docs/commercial.md
Normal file
84
docSite/content/docs/commercial.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: '商业版'
|
||||
description: 'FastGPT 商业版相关说明'
|
||||
icon: 'shopping_cart'
|
||||
draft: false
|
||||
toc: true
|
||||
weight: 20
|
||||
---
|
||||
|
||||
## FastGPT 线上服务
|
||||
|
||||
[按线上标准计费](/docs/pricing)即可。 地址: https://fastgpt.run
|
||||
|
||||
## 商业版
|
||||
|
||||
商业版最终交付版本功能与 https://fastgpt.run 完全一致,品牌内容可自定义。
|
||||
|
||||
{{% alert icon="🤖" context="warning" %}}
|
||||
商业版与开源版功能差异:(目前计划)
|
||||
|
||||
1. 自定义 title 和 logo
|
||||
2. 用户注册,支付 (已有微信扫码支付,后续会补充支付方式)
|
||||
3. 团队空间 (下期开发)
|
||||
4. 完善的 OpenAPI
|
||||
5. 高级编排额外插件
|
||||
6. 后台管理系统 (已有,持续更新)
|
||||
{{% /alert %}}
|
||||
|
||||
### 商业版定价
|
||||
|
||||
#### 交付费用
|
||||
|
||||
+ 使用 [Sealos 公有云](https://sealos.io)交付:1w/年/套 (直接在 Sealos 公有云充值,便可**免费获取 FastGPT 商业版 License**,同时您充值的金额可用于部署其他云资源,相当于白嫖了一个 FastGPT 商业版)。
|
||||
+ 渠道商使用 Sealos 交付:返现 20% 成交额。
|
||||
+ 私有服务器交付:2w/年/套(如需部署支持,按技术服务费计算)
|
||||
+ 渠道商私有服务器交付:1.3w/年/套(渠道商合同单独约谈,累计 5 套以上可签)
|
||||
|
||||
#### 用户注册数量费用(按注册量算,不计量分享和 API)
|
||||
|
||||
{{< table "table-hover table-striped-columns" >}}
|
||||
| 最大用户数量 | 费用 元/年 |
|
||||
| ------------ | ---------- |
|
||||
| 100 | 0 |
|
||||
| 5000 | 5000 |
|
||||
| 2万 | 18000 |
|
||||
| 5万 | 35000 |
|
||||
| 20万 | 100000 |
|
||||
{{< /table >}}
|
||||
|
||||
#### 总费用
|
||||
|
||||
总费用 = 商业版功能费用 + 用户数量费用
|
||||
|
||||
## 技术支持
|
||||
|
||||
### 应用定制
|
||||
|
||||
根据需求,定制实现某个需求的编排功能,最终会交付一个应用编排。可根据实际情况商讨。
|
||||
|
||||
### 技术服务费(定开、部署、维护等)
|
||||
|
||||
2000元/人/天
|
||||
|
||||
### 更新费用
|
||||
|
||||
大部分更新,重新拉镜像就可以了,不需要执行额外操作。
|
||||
|
||||
复杂更新可参考文档自行更新;或付费支持,标准与技术服务费一致。
|
||||
|
||||
## 联系方式
|
||||
|
||||
通过邮箱联系 yujinlong@sealos.io
|
||||
|
||||
## QA
|
||||
|
||||
1. 如何交付?
|
||||
|
||||
完整版应用 = 开源版镜像 + 商业版镜像
|
||||
|
||||
我们会提供一个商业版镜像给你使用,还会提供一个简单的后台管理系统(目前只设置了简单的查询功能)
|
||||
|
||||
2. 二次开发如何操作?
|
||||
|
||||
可自行修改开源版代码进行二次开发,不支持修改商业版镜像。
|
||||
8
docSite/content/docs/community/_index.md
Normal file
8
docSite/content/docs/community/_index.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
weight: 1100
|
||||
title: '社区'
|
||||
description: '社区相关内容'
|
||||
icon: 'forum'
|
||||
draft: false
|
||||
images: []
|
||||
---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user