mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
v4.2 (#217)
* fix: chat module link * fix: url fetch check * fix: import file ui * feat: app logs * perf: iframe icon * imgs cdn * perf: click range and pg
This commit is contained in:
@@ -47,7 +47,7 @@ FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开
|
||||
- [x] 支持直接分段导入
|
||||
- [x] 支持 QA 拆分导入
|
||||
- [x] 支持手动输入内容
|
||||
- [ ] 支持 url 读取导入
|
||||
- [x] 支持 url 读取导入
|
||||
- [x] 支持 CSV 批量导入问答对
|
||||
- [ ] 支持知识库单独设置向量模型
|
||||
- [ ] 源文件存储
|
||||
|
@@ -15,10 +15,10 @@
|
||||
"@dqbd/tiktoken": "^1.0.7",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mozilla/readability": "^0.4.4",
|
||||
"@next/font": "13.1.6",
|
||||
"@tanstack/react-query": "^4.24.10",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@mozilla/readability": "^0.4.4",
|
||||
"axios": "^1.3.3",
|
||||
"cookie": "^0.5.0",
|
||||
"crypto": "^1.0.1",
|
||||
@@ -31,6 +31,7 @@
|
||||
"i18next": "^22.5.1",
|
||||
"immer": "^9.0.19",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsdom": "^22.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mammoth": "^1.5.1",
|
||||
@@ -59,7 +60,6 @@
|
||||
"request-ip": "^3.3.0",
|
||||
"sass": "^1.58.3",
|
||||
"tunnel": "^0.0.6",
|
||||
"jsdom": "^22.1.0",
|
||||
"winston": "^3.10.0",
|
||||
"winston-mongodb": "^5.1.1",
|
||||
"zustand": "^4.3.5"
|
||||
@@ -69,6 +69,7 @@
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "18.14.0",
|
||||
@@ -79,7 +80,6 @@
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@types/request-ip": "^0.0.37",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"typescript": "4.9.5"
|
||||
|
@@ -1,7 +1,8 @@
|
||||
### Fast GPT V4.1
|
||||
### Fast GPT V4.2
|
||||
|
||||
1. 新增 - 高级编排导入导出功能
|
||||
2. 优化对话存储结构
|
||||
3. 优化日志存储
|
||||
4. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
|
||||
5. 填写个人 OpenAI 账号后,分享和 API 功能也不会走平台余额扣费。
|
||||
1. 新增 - 应用日志初版,可观测到所有对话记录
|
||||
2. 新增 - 好友邀请链接,[点击查看](/account?currentTab=promotion)
|
||||
3. 新增 - Iframe 嵌入页面图标可拖拽
|
||||
4. 优化 - 知识库搜索提示词
|
||||
5. 优化 - [使用文档](https://doc.fastgpt.run/docs/intro/)
|
||||
6. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
|
||||
|
@@ -20,11 +20,9 @@ async function embedChatbot() {
|
||||
const ChatBtn = document.createElement('div');
|
||||
ChatBtn.id = chatBtnId;
|
||||
ChatBtn.style.cssText =
|
||||
'position: fixed; bottom: 1rem; right: 1rem; width: 40px; height: 40px; cursor: pointer; z-index: 2147483647; ';
|
||||
'position: fixed; bottom: 1rem; right: 1rem; width: 40px; height: 40px; cursor: pointer; z-index: 2147483647; transition: 0;';
|
||||
|
||||
const ChatBtnDiv = document.createElement('div');
|
||||
ChatBtnDiv.style.cssText =
|
||||
'transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);} display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 9999;';
|
||||
ChatBtnDiv.innerHTML = MessageIcon;
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
@@ -38,7 +36,15 @@ async function embedChatbot() {
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
let chatBtnDragged = false;
|
||||
let chatBtnDown = false;
|
||||
let chatBtnMouseX;
|
||||
let chatBtnMouseY;
|
||||
ChatBtn.addEventListener('click', function () {
|
||||
if (chatBtnDragged) {
|
||||
chatBtnDragged = false;
|
||||
return;
|
||||
}
|
||||
const chatWindow = document.getElementById(chatWindowId);
|
||||
|
||||
if (!chatWindow) return;
|
||||
@@ -52,6 +58,31 @@ async function embedChatbot() {
|
||||
}
|
||||
});
|
||||
|
||||
ChatBtn.addEventListener('mousedown', (e) => {
|
||||
if (!chatBtnMouseX && !chatBtnMouseY) {
|
||||
chatBtnMouseX = e.clientX;
|
||||
chatBtnMouseY = e.clientY;
|
||||
}
|
||||
|
||||
chatBtnDown = true;
|
||||
});
|
||||
ChatBtn.addEventListener('mousemove', (e) => {
|
||||
if (!chatBtnDown) return;
|
||||
chatBtnDragged = true;
|
||||
const transformX = e.clientX - chatBtnMouseX;
|
||||
const transformY = e.clientY - chatBtnMouseY;
|
||||
|
||||
ChatBtn.style.transform = `translate3d(${transformX}px, ${transformY}px, 0)`;
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
ChatBtn.addEventListener('mouseup', (e) => {
|
||||
chatBtnDown = false;
|
||||
});
|
||||
ChatBtn.addEventListener('mouseleave', (e) => {
|
||||
chatBtnDown = false;
|
||||
});
|
||||
|
||||
ChatBtn.appendChild(ChatBtnDiv);
|
||||
document.body.appendChild(ChatBtn);
|
||||
}
|
||||
|
@@ -4,9 +4,12 @@
|
||||
"Confirm": "Yes",
|
||||
"Running": "Running",
|
||||
"Warning": "Warning",
|
||||
"UnKnow": "UnKnow",
|
||||
"app": {
|
||||
"Advance App TestTip": "The current application is advanced editing mode \n. If you need to switch to [simple mode], please click the save button on the left",
|
||||
"App Detail": "App Detail",
|
||||
"Chat Logs Tips": "Logs record the app's online, shared, and API conversations",
|
||||
"Chat logs": "Chat Logs",
|
||||
"Confirm Del App Tip": "Confirm to delete the app and all its chats",
|
||||
"Confirm Save App Tip": "The application may be in advanced orchestration mode, and the advanced orchestration configuration will be overwritten after saving, please confirm!",
|
||||
"Connection is invalid": "Connecting is invalid",
|
||||
@@ -18,6 +21,11 @@
|
||||
"Import Config Failed": "Failed to import the configuration, please ensure that the configuration is normal!",
|
||||
"Import Configs": "Import Configs",
|
||||
"Input Field Settings": "Input Field Settings",
|
||||
"Logs Empty": "Logs is empty",
|
||||
"Logs Message Total": "Message Count",
|
||||
"Logs Source": "Source",
|
||||
"Logs Time": "Time",
|
||||
"Logs Title": "Title",
|
||||
"My Apps": "My Apps",
|
||||
"Output Field Settings": "Output Field Settings",
|
||||
"Paste Config": "Paste Config"
|
||||
@@ -28,7 +36,13 @@
|
||||
"Exit Chat": "Exit",
|
||||
"History": "History",
|
||||
"New Chat": "New Chat",
|
||||
"You need to a chat app": "You need to a chat app"
|
||||
"You need to a chat app": "You need to a chat app",
|
||||
"logs": {
|
||||
"api": "API",
|
||||
"online": "Online Chat",
|
||||
"share": "Share",
|
||||
"test": "Test Chat "
|
||||
}
|
||||
},
|
||||
"commom": {
|
||||
"Password inconsistency": "Password inconsistency"
|
||||
@@ -149,6 +163,10 @@
|
||||
"Update Password": "Update Password",
|
||||
"Update password failed": "Update password failed",
|
||||
"Update password succseful": "Update password succseful",
|
||||
"Usage Record": "Usage"
|
||||
"Usage Record": "Usage",
|
||||
"promotion": {
|
||||
"pay": "",
|
||||
"register": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,9 +4,12 @@
|
||||
"Confirm": "确认",
|
||||
"Running": "运行中",
|
||||
"Warning": "提示",
|
||||
"UnKnow": "未知",
|
||||
"app": {
|
||||
"Advance App TestTip": "当前应用为高级编排模式\n如需切换为【简易模式】请点击左侧保存按键",
|
||||
"App Detail": "应用详情",
|
||||
"Chat Logs Tips": "日志会记录该应用的在线、分享和 API 对话记录",
|
||||
"Chat logs": "对话日志",
|
||||
"Confirm Del App Tip": "确认删除该应用及其所有聊天记录?",
|
||||
"Confirm Save App Tip": "该应用可能为高级编排模式,保存后将会覆盖高级编排配置,请确认!",
|
||||
"Connection is invalid": "连接无效",
|
||||
@@ -18,6 +21,11 @@
|
||||
"Import Config Failed": "导入配置失败,请确保配置正常!",
|
||||
"Import Configs": "导入配置",
|
||||
"Input Field Settings": "输入字段编辑",
|
||||
"Logs Empty": "还没有日志噢~",
|
||||
"Logs Message Total": "消息总数",
|
||||
"Logs Source": "来源",
|
||||
"Logs Time": "时间",
|
||||
"Logs Title": "标题",
|
||||
"My Apps": "我的应用",
|
||||
"Output Field Settings": "输出字段编辑",
|
||||
"Paste Config": "粘贴配置"
|
||||
@@ -28,7 +36,13 @@
|
||||
"Exit Chat": "退出聊天",
|
||||
"History": "记录",
|
||||
"New Chat": "新对话",
|
||||
"You need to a chat app": "你需要创建一个应用"
|
||||
"You need to a chat app": "你需要创建一个应用",
|
||||
"logs": {
|
||||
"api": "API 调用",
|
||||
"online": "在线使用",
|
||||
"share": "外部链接调用",
|
||||
"test": "测试"
|
||||
}
|
||||
},
|
||||
"commom": {
|
||||
"Password inconsistency": "两次密码不一致"
|
||||
@@ -151,8 +165,8 @@
|
||||
"Update password succseful": "修改密码成功",
|
||||
"Usage Record": "使用记录",
|
||||
"promotion": {
|
||||
"register": "好友注册",
|
||||
"pay": "好友充值"
|
||||
"pay": "好友充值",
|
||||
"register": "好友注册"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -51,3 +51,6 @@ export const getAppTotalUsage = (data: { appId: string }) =>
|
||||
start: addDays(new Date(), -13),
|
||||
end: addDays(new Date(), 1)
|
||||
}).then((res) => (res.length === 0 ? [{ date: new Date(), total: 0 }] : res));
|
||||
|
||||
export const getAppChatLogs = (data: RequestPaging & { appId: string }) =>
|
||||
POST(`/chat/getChatLogs`, data);
|
||||
|
@@ -96,6 +96,7 @@ const QuoteModal = ({
|
||||
_notLast={{ mb: 2 }}
|
||||
position={'relative'}
|
||||
_hover={{ '& .edit': { display: 'flex' } }}
|
||||
overflow={'hidden'}
|
||||
>
|
||||
{item.source && <Box color={'myGray.600'}>({item.source})</Box>}
|
||||
<Box>{item.q}</Box>
|
||||
|
@@ -141,7 +141,7 @@ const ChatBox = (
|
||||
variableModules?: VariableItemType[];
|
||||
welcomeText?: string;
|
||||
onUpdateVariable?: (e: Record<string, any>) => void;
|
||||
onStartChat: (e: StartChatFnProps) => Promise<{
|
||||
onStartChat?: (e: StartChatFnProps) => Promise<{
|
||||
responseText: string;
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType[];
|
||||
}>;
|
||||
@@ -239,8 +239,7 @@ const ChatBox = (
|
||||
// 复制内容
|
||||
const onclickCopy = useCallback(
|
||||
(value: string) => {
|
||||
const val = value.replace(/\n+/g, '\n');
|
||||
copyData(val);
|
||||
copyData(value);
|
||||
},
|
||||
[copyData]
|
||||
);
|
||||
@@ -264,6 +263,7 @@ const ChatBox = (
|
||||
*/
|
||||
const sendPrompt = useCallback(
|
||||
async (variables: Record<string, any> = {}, inputVal = '') => {
|
||||
if (!onStartChat) return;
|
||||
if (isChatting) {
|
||||
toast({
|
||||
title: '正在聊天中...请等待结束',
|
||||
@@ -272,7 +272,7 @@ const ChatBox = (
|
||||
return;
|
||||
}
|
||||
// get input value
|
||||
const val = inputVal.trim().replace(/\n\s*/g, '\n');
|
||||
const val = inputVal.trim();
|
||||
|
||||
if (!val) {
|
||||
toast({
|
||||
@@ -698,7 +698,7 @@ const ChatBox = (
|
||||
</Box>
|
||||
</Box>
|
||||
{/* input */}
|
||||
{variableIsFinish ? (
|
||||
{onStartChat && variableIsFinish ? (
|
||||
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']} px={[0, 5]}>
|
||||
<Box
|
||||
py={'18px'}
|
||||
|
14
client/src/components/Icon/icons/light/logs.svg
Normal file
14
client/src/components/Icon/icons/light/logs.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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="1692867122828"
|
||||
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4021"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64">
|
||||
<path
|
||||
d="M269.844659 81.4308h44.821057v166.626082h-44.821057zM677.140966 491.719232c52.335426 0 102.092273 19.937769 140.105639 56.13883 38.126482 36.31053 60.461599 85.284073 62.891788 137.900467 2.5056 54.276658-16.27424 106.280032-52.881549 146.431672-36.60731 40.15164-86.65972 63.643469-140.936379 66.150285-3.180653 0.147174-6.401444 0.221369-9.576016 0.221369-52.341508 0-102.102004-19.936552-140.114153-56.136398-38.126482-36.309314-60.461599-85.284073-62.891789-137.902899-2.5056-54.276658 16.27424-106.280032 52.88155-146.431672 36.60731-40.15164 86.65972-63.643469 140.936379-66.149069a208.122961 208.122961 0 0 1 9.576016-0.221369h0.008514m-0.00973-44.822274c-3.859355 0-7.746684 0.088791-11.642528 0.268805-136.951744 6.3236-242.847422 122.470346-236.525038 259.422091 6.143586 133.0559 115.942406 236.793842 247.779562 236.793842 3.859355 0 7.747901-0.088791 11.642529-0.268804 136.951744-6.322384 242.847422-122.470346 236.525037-259.422091-6.143586-133.057117-115.942406-236.798708-247.779562-236.793843z"
|
||||
p-id="4022"></path>
|
||||
<path
|
||||
d="M490.264524 891.110734a272.361206 272.361206 0 0 1-32.682275-37.369937H180.453104c-20.912034 0-37.927007-17.013757-37.927007-37.92579v-590.263526c0-20.912034 17.013757-37.927007 37.927007-37.927007H732.799354c20.912034 0 37.925791 17.013757 37.925791 37.927007V441.15597a268.605238 268.605238 0 0 1 44.821057 21.463023V225.551481c0-45.70045-37.047614-82.746848-82.746848-82.746849H180.453104c-45.70045 0-82.746848 37.047614-82.746848 82.746849v590.263526c0 45.70045 37.047614 82.746848 82.746848 82.746848h317.980164a273.587248 273.587248 0 0 1-8.168744-7.451121z"
|
||||
p-id="4023"></path>
|
||||
<path
|
||||
d="M770.725145 489.61623a225.243754 225.243754 0 0 1 44.821057 27.231985v-0.21407a225.182938 225.182938 0 0 0-44.821057-27.114003v0.096088zM812.590566 778.530212H646.820768V576.105667h44.821057v157.604704h120.948741zM209.55091 380.121489h498.255687v44.821057H209.55091zM600.682445 81.4308h44.821058v166.626082h-44.821058zM406.842623 712.17437H209.55091v44.821057h203.864657a272.351476 272.351476 0 0 1-6.572944-44.821057zM450.941192 546.147929H209.55091v44.821057h217.435038a268.707408 268.707408 0 0 1 23.955244-44.821057z"
|
||||
p-id="4024"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
@@ -76,7 +76,8 @@ const map = {
|
||||
addCircle: require('./icons/circle/add.svg').default,
|
||||
playFill: require('./icons/fill/play.svg').default,
|
||||
courseLight: require('./icons/light/course.svg').default,
|
||||
promotionLight: require('./icons/light/promotion.svg').default
|
||||
promotionLight: require('./icons/light/promotion.svg').default,
|
||||
logsLight: require('./icons/light/logs.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
@@ -31,7 +31,7 @@ export const ChatRoleMap = {
|
||||
};
|
||||
|
||||
export enum ChatSourceEnum {
|
||||
'test' = 'test',
|
||||
test = 'test',
|
||||
online = 'online',
|
||||
share = 'share',
|
||||
api = 'api'
|
||||
@@ -39,16 +39,16 @@ export enum ChatSourceEnum {
|
||||
|
||||
export const ChatSourceMap = {
|
||||
[ChatSourceEnum.test]: {
|
||||
name: '调试测试'
|
||||
name: 'chat.logs.test'
|
||||
},
|
||||
[ChatSourceEnum.online]: {
|
||||
name: '在线使用'
|
||||
name: 'chat.logs.online'
|
||||
},
|
||||
[ChatSourceEnum.share]: {
|
||||
name: '链接分享'
|
||||
name: 'chat.logs.share'
|
||||
},
|
||||
[ChatSourceEnum.api]: {
|
||||
name: 'API调用'
|
||||
name: 'chat.logs.api'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,7 +56,8 @@ export enum ChatModuleEnum {
|
||||
'AIChat' = 'AI Chat',
|
||||
'KBSearch' = 'KB Search',
|
||||
'CQ' = 'Classify Question',
|
||||
'Extract' = 'Content Extract'
|
||||
'Extract' = 'Content Extract',
|
||||
'Http' = 'Http'
|
||||
}
|
||||
|
||||
export enum OutLinkTypeEnum {
|
||||
|
@@ -158,7 +158,8 @@ export const ChatModule: FlowModuleTemplateType = {
|
||||
label: '系统提示词',
|
||||
valueType: FlowValueTypeEnum.string,
|
||||
description: ChatModelSystemTip,
|
||||
placeholder: ChatModelSystemTip
|
||||
placeholder: ChatModelSystemTip,
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'limitPrompt',
|
||||
@@ -166,7 +167,8 @@ export const ChatModule: FlowModuleTemplateType = {
|
||||
valueType: FlowValueTypeEnum.string,
|
||||
label: '限定词',
|
||||
description: ChatModelLimitTip,
|
||||
placeholder: ChatModelLimitTip
|
||||
placeholder: ChatModelLimitTip,
|
||||
value: ''
|
||||
},
|
||||
Input_Template_TFSwitch,
|
||||
{
|
||||
@@ -281,6 +283,7 @@ export const AnswerModule: FlowModuleTemplateType = {
|
||||
key: SpecialInputKeyEnum.answerText,
|
||||
type: FlowInputItemTypeEnum.textarea,
|
||||
valueType: FlowValueTypeEnum.string,
|
||||
value: '',
|
||||
label: '回复的内容',
|
||||
description:
|
||||
'可以使用 \\n 来实现换行。也可以通过外部模块输入实现回复,外部模块输入时会覆盖当前填写的内容'
|
||||
@@ -337,6 +340,7 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = {
|
||||
key: 'systemPrompt',
|
||||
type: FlowInputItemTypeEnum.textarea,
|
||||
valueType: FlowValueTypeEnum.string,
|
||||
value: '',
|
||||
label: '系统提示词',
|
||||
description:
|
||||
'你可以添加一些特定内容的介绍,从而更好的识别用户的问题类型。这个内容通常是给模型介绍一个它不知道的内容。',
|
||||
@@ -398,6 +402,7 @@ export const ContextExtractModule: FlowModuleTemplateType = {
|
||||
key: ContextExtractEnum.description,
|
||||
type: FlowInputItemTypeEnum.textarea,
|
||||
valueType: FlowValueTypeEnum.string,
|
||||
value: '',
|
||||
label: '提取要求描述',
|
||||
description: '写一段提取要求,告诉 AI 需要提取哪些内容',
|
||||
required: true,
|
||||
|
@@ -28,7 +28,7 @@ const queryClient = new QueryClient({
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
cacheTime: 0
|
||||
cacheTime: 10
|
||||
}
|
||||
}
|
||||
});
|
||||
|
72
client/src/pages/api/chat/getChatLogs.ts
Normal file
72
client/src/pages/api/chat/getChatLogs.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { Chat, ChatItem, connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import type { PagingData } from '@/types';
|
||||
import { AppLogsListItemType } from '@/types/app';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const {
|
||||
pageNum = 1,
|
||||
pageSize = 20,
|
||||
appId
|
||||
} = req.body as { pageNum: number; pageSize: number; appId: string };
|
||||
|
||||
if (!appId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const where = {
|
||||
appId: new Types.ObjectId(appId),
|
||||
userId: new Types.ObjectId(userId)
|
||||
};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
Chat.aggregate([
|
||||
{ $match: where },
|
||||
{ $sort: { updateTime: -1 } },
|
||||
{ $skip: (pageNum - 1) * pageSize },
|
||||
{ $limit: pageSize },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'chatitems',
|
||||
localField: 'chatId',
|
||||
foreignField: 'chatId',
|
||||
as: 'messageCount'
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
id: '$chatId',
|
||||
title: 1,
|
||||
source: 1,
|
||||
time: '$updateTime',
|
||||
messageCount: { $size: '$messageCount' },
|
||||
callbackCount: { $literal: 0 }
|
||||
}
|
||||
}
|
||||
]),
|
||||
Chat.countDocuments(where)
|
||||
]);
|
||||
|
||||
jsonRes<PagingData<AppLogsListItemType>>(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data,
|
||||
total
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
@@ -27,6 +27,7 @@ import { pushTaskBill } from '@/service/events/pushBill';
|
||||
import { BillSourceEnum } from '@/constants/user';
|
||||
import { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
|
||||
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
|
||||
type FastGptWebChatProps = {
|
||||
@@ -302,6 +303,8 @@ export async function dispatchModules({
|
||||
|
||||
if (!set.has(module.moduleId) && checkInputFinish()) {
|
||||
set.add(module.moduleId);
|
||||
// remove switch
|
||||
updateInputValue(SystemInputEnum.switch, undefined);
|
||||
return moduleRun(module);
|
||||
}
|
||||
})
|
||||
@@ -324,6 +327,7 @@ export async function dispatchModules({
|
||||
// find module
|
||||
const targetModule = runningModules.find((item) => item.moduleId === target.moduleId);
|
||||
if (!targetModule) return;
|
||||
|
||||
return moduleInput(targetModule, { [target.key]: outputItem.value });
|
||||
})
|
||||
);
|
||||
@@ -332,7 +336,6 @@ export async function dispatchModules({
|
||||
}
|
||||
async function moduleRun(module: RunningModuleItemType): Promise<any> {
|
||||
if (res.closed) return Promise.resolve();
|
||||
// console.log('run=========', module.flowType);
|
||||
|
||||
if (stream && detail && module.showStatus) {
|
||||
responseStatus({
|
||||
|
@@ -12,7 +12,7 @@ export type UrlFetchResponse = FetchResultItem[];
|
||||
|
||||
const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const { urlList = [] } = req.body as { urlList: string[] };
|
||||
let { urlList = [] } = req.body as { urlList: string[] };
|
||||
|
||||
if (!urlList || urlList.length === 0) {
|
||||
throw new Error('urlList is empty');
|
||||
@@ -20,9 +20,12 @@ const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
await authUser({ req });
|
||||
|
||||
urlList = urlList.filter((url) => /^(http|https):\/\/[^ "]+$/.test(url));
|
||||
|
||||
const response = (
|
||||
await Promise.allSettled(
|
||||
urlList.map(async (url) => {
|
||||
try {
|
||||
const fetchRes = await axios.get(url, {
|
||||
timeout: 30000
|
||||
});
|
||||
@@ -41,6 +44,12 @@ const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
url,
|
||||
content: simpleText(`${article?.title}\n${content}`)
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
url,
|
||||
content: ''
|
||||
};
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
|
237
client/src/pages/app/detail/components/Logs.tsx
Normal file
237
client/src/pages/app/detail/components/Logs.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useTheme
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { getAppChatLogs } from '@/api/app';
|
||||
import dayjs from 'dayjs';
|
||||
import { ChatSourceMap, HUMAN_ICON } from '@/constants/chat';
|
||||
import { AppLogsListItemType } from '@/types/app';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import ChatBox, { type ComponentRef } from '@/components/ChatBox';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getInitChatSiteInfo } from '@/api/chat';
|
||||
import Tag from '@/components/Tag';
|
||||
|
||||
const Logs = ({ appId }: { appId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const {
|
||||
data: logs,
|
||||
isLoading,
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum
|
||||
} = usePagination<AppLogsListItemType>({
|
||||
api: getAppChatLogs,
|
||||
pageSize: 20,
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
const [detailLogsId, setDetailLogsId] = useState<string>();
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} pt={[1, 5]} position={'relative'}>
|
||||
<Box px={[4, 8]}>
|
||||
{isPc && (
|
||||
<>
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'xl']} mb={2}>
|
||||
{t('app.Chat logs')}
|
||||
</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{t('app.Chat Logs Tips')}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* table */}
|
||||
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'} px={[4, 8]}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('app.Logs Source')}</Th>
|
||||
<Th>{t('app.Logs Time')}</Th>
|
||||
<Th>{t('app.Logs Title')}</Th>
|
||||
<Th>{t('app.Logs Message Total')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{logs.map((item) => (
|
||||
<Tr
|
||||
key={item.id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={'点击查看对话详情'}
|
||||
onClick={() => setDetailLogsId(item.id)}
|
||||
>
|
||||
<Td>{t(ChatSourceMap[item.source]?.name || 'UnKnow')}</Td>
|
||||
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Td>
|
||||
<Td className="textEllipsis" maxW={'250px'}>
|
||||
{item.title}
|
||||
</Td>
|
||||
<Td>{item.messageCount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box p={4}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
{logs.length === 0 && !isLoading && (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'10vh'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t('app.Logs Empty')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{!!detailLogsId && (
|
||||
<DetailLogsModal
|
||||
appId={appId}
|
||||
chatId={detailLogsId}
|
||||
onClose={() => setDetailLogsId(undefined)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
|
||||
function DetailLogsModal({
|
||||
appId,
|
||||
chatId,
|
||||
onClose
|
||||
}: {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
const { isPc } = useGlobalStore();
|
||||
const theme = useTheme();
|
||||
|
||||
const { data: chat } = useQuery(
|
||||
['getChatDetail', chatId],
|
||||
() => getInitChatSiteInfo({ appId, chatId }),
|
||||
{
|
||||
onSuccess(res) {
|
||||
const history = res.history.map((item) => ({
|
||||
...item,
|
||||
status: 'finish' as any
|
||||
}));
|
||||
ChatBoxRef.current?.resetHistory(history);
|
||||
ChatBoxRef.current?.resetVariables(res.variables);
|
||||
if (res.history.length > 0) {
|
||||
setTimeout(() => {
|
||||
ChatBoxRef.current?.scrollToBottom('auto');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const history = useMemo(() => (chat?.history ? chat.history : []), [chat]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
return history[history.length - 2]?.value?.slice(0, 8);
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={['fixed', 'absolute']}
|
||||
top={[0, '2%']}
|
||||
right={0}
|
||||
h={['100%', '96%']}
|
||||
w={'100%'}
|
||||
maxW={['100%', '600px']}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={[3, 5]}
|
||||
h={['46px', '60px']}
|
||||
borderBottom={theme.borders.base}
|
||||
borderBottomColor={'gray.200'}
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{isPc ? (
|
||||
<>
|
||||
<Box mr={3} color={'myGray.1000'}>
|
||||
{title}
|
||||
</Box>
|
||||
<Tag>
|
||||
<MyIcon name={'history'} w={'14px'} />
|
||||
<Box ml={1}>{`${history.length}条记录`}</Box>
|
||||
</Tag>
|
||||
{!!chat?.app?.chatModels && (
|
||||
<Tag ml={2} colorSchema={'green'}>
|
||||
<MyIcon name={'chatModelTag'} w={'14px'} />
|
||||
<Box ml={1}>{chat.app.chatModels.join(',')}</Box>
|
||||
</Tag>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Flex px={3} alignItems={'center'} flex={'1 0 0'} w={0} justifyContent={'center'}>
|
||||
<Box ml={1} className="textEllipsis">
|
||||
{title}
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
w={'20px'}
|
||||
h={'20px'}
|
||||
borderRadius={'50%'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
onClick={onClose}
|
||||
>
|
||||
<MyIcon name={'closeLight'} w={'12px'} h={'12px'} color={'myGray.700'} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box pt={2} flex={'1 0 0'}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
chatId={chatId}
|
||||
appAvatar={chat?.app.avatar}
|
||||
userAvatar={HUMAN_ICON}
|
||||
variableModules={chat?.app.variableModules}
|
||||
welcomeText={chat?.app.welcomeText}
|
||||
onUpdateVariable={(e) => {}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -26,11 +26,15 @@ const OutLink = dynamic(() => import('./components/OutLink'), {
|
||||
const API = dynamic(() => import('./components/API'), {
|
||||
ssr: false
|
||||
});
|
||||
const Logs = dynamic(() => import('./components/Logs'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
enum TabEnum {
|
||||
'basicEdit' = 'basicEdit',
|
||||
'adEdit' = 'adEdit',
|
||||
'outLink' = 'outLink',
|
||||
'logs' = 'logs',
|
||||
'API' = 'API'
|
||||
}
|
||||
|
||||
@@ -59,6 +63,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
{ label: '高级编排', id: TabEnum.adEdit, icon: 'settingLight' },
|
||||
{ label: '外部使用', id: TabEnum.outLink, icon: 'shareLight' },
|
||||
{ label: 'API访问', id: TabEnum.API, icon: 'apiLight' },
|
||||
{ label: '对话日志', id: TabEnum.logs, icon: 'logsLight' },
|
||||
{ label: '立即对话', id: 'startChat', icon: 'chat' }
|
||||
],
|
||||
[]
|
||||
@@ -148,7 +153,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* phone tab */}
|
||||
<Box display={['block', 'none']} textAlign={'center'} px={5} py={3}>
|
||||
<Box display={['block', 'none']} textAlign={'center'} py={3}>
|
||||
<Box className="textlg" fontSize={'xl'} fontWeight={'bold'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
@@ -174,6 +179,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
<AdEdit app={appDetail} onCloseSettings={() => setCurrentTab(TabEnum.basicEdit)} />
|
||||
)}
|
||||
{currentTab === TabEnum.API && <API appId={appId} />}
|
||||
{currentTab === TabEnum.logs && <Logs appId={appId} />}
|
||||
{currentTab === TabEnum.outLink && <OutLink appId={appId} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
@@ -132,7 +132,7 @@ const ChunkImport = ({ kbId }: { kbId: string }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']}>
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']} overflow={'overlay'}>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
flex={'1 0 0'}
|
||||
@@ -152,7 +152,7 @@ const ChunkImport = ({ kbId }: { kbId: string }) => {
|
||||
|
||||
{!emptyFiles && (
|
||||
<>
|
||||
<Box py={4} px={2} maxH={'400px'} overflow={'auto'}>
|
||||
<Box py={4} px={2} minH={['auto', '100px']} maxH={'400px'} overflow={'auto'}>
|
||||
{files.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
|
@@ -77,7 +77,7 @@ const CsvImport = ({ kbId }: { kbId: string }) => {
|
||||
maxW: '400px'
|
||||
};
|
||||
return (
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']}>
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']} overflow={'overlay'}>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
flex={'1 0 0'}
|
||||
@@ -100,7 +100,7 @@ const CsvImport = ({ kbId }: { kbId: string }) => {
|
||||
|
||||
{!emptyFiles && (
|
||||
<>
|
||||
<Box py={4} px={2} maxH={'400px'} overflow={'auto'}>
|
||||
<Box py={4} minH={['auto', '100px']} px={2} maxH={'400px'} overflow={'auto'}>
|
||||
{files.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
|
@@ -65,7 +65,7 @@ const ManualImport = ({ kbId }: { kbId: string }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Box p={[4, 8]}>
|
||||
<Box p={[4, 8]} h={'100%'} overflow={'overlay'}>
|
||||
<Box display={'flex'} flexDirection={['column', 'row']}>
|
||||
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']}>
|
||||
<Box h={'30px'}>{'匹配的知识点'}</Box>
|
||||
|
@@ -123,7 +123,7 @@ const QAImport = ({ kbId }: { kbId: string }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']}>
|
||||
<Box display={['block', 'flex']} h={['auto', '100%']} overflow={'overlay'}>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
flex={'1 0 0'}
|
||||
@@ -143,7 +143,7 @@ const QAImport = ({ kbId }: { kbId: string }) => {
|
||||
|
||||
{!emptyFiles && (
|
||||
<>
|
||||
<Box py={4} px={2} maxH={'400px'} overflow={'auto'}>
|
||||
<Box py={4} px={2} minH={['auto', '100px']} maxH={'400px'} overflow={'auto'}>
|
||||
{files.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
|
@@ -64,6 +64,7 @@ try {
|
||||
ChatItemSchema.index({ time: -1 });
|
||||
ChatItemSchema.index({ userId: 1 });
|
||||
ChatItemSchema.index({ appId: 1 });
|
||||
ChatItemSchema.index({ chatId: 1 });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@@ -10,6 +10,6 @@ export const dispatchHistory = (props: Record<string, any>) => {
|
||||
const { maxContext = 5, history = [] } = props as HistoryProps;
|
||||
|
||||
return {
|
||||
history: history.slice(-maxContext)
|
||||
history: maxContext > 0 ? history.slice(-maxContext) : []
|
||||
};
|
||||
};
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { ChatModuleEnum, TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { HttpPropsEnum } from '@/constants/flow/flowField';
|
||||
import { ChatHistoryItemResType } from '@/types/chat';
|
||||
import type { NextApiResponse } from 'next';
|
||||
|
||||
export type HttpRequestProps = {
|
||||
@@ -11,6 +13,7 @@ export type HttpRequestProps = {
|
||||
export type HttpResponse = {
|
||||
[HttpPropsEnum.finish]: boolean;
|
||||
[HttpPropsEnum.failed]?: boolean;
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
@@ -22,12 +25,22 @@ export const dispatchHttpRequest = async (props: Record<string, any>): Promise<H
|
||||
|
||||
return {
|
||||
[HttpPropsEnum.finish]: true,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.Http,
|
||||
price: 0,
|
||||
httpResult: response
|
||||
},
|
||||
...response
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
[HttpPropsEnum.finish]: true,
|
||||
[HttpPropsEnum.failed]: true
|
||||
[HttpPropsEnum.failed]: true,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.Http,
|
||||
price: 0,
|
||||
httpResult: {}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ import { Pool } from 'pg';
|
||||
import type { QueryResultRow } from 'pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
|
||||
export const connectPg = async () => {
|
||||
export const connectPg = async (): Promise<Pool> => {
|
||||
if (global.pgClient) {
|
||||
return global.pgClient;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export const connectPg = async () => {
|
||||
global.pgClient.on('error', (err) => {
|
||||
console.log(err);
|
||||
global.pgClient = null;
|
||||
connectPg();
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -25,7 +26,7 @@ export const connectPg = async () => {
|
||||
return global.pgClient;
|
||||
} catch (error) {
|
||||
global.pgClient = null;
|
||||
return Promise.reject(error);
|
||||
return connectPg();
|
||||
}
|
||||
};
|
||||
|
||||
|
11
client/src/types/app.d.ts
vendored
11
client/src/types/app.d.ts
vendored
@@ -7,7 +7,7 @@ import {
|
||||
VariableInputEnum
|
||||
} from '../constants/app';
|
||||
import type { FlowInputItemType, FlowOutputItemType, FlowOutputTargetItemType } from './flow';
|
||||
import type { AppSchema, kbSchema } from './mongoSchema';
|
||||
import type { AppSchema, ChatSchema, kbSchema } from './mongoSchema';
|
||||
import { ChatModelType } from '@/constants/model';
|
||||
import { FlowValueTypeEnum } from '@/constants/flow';
|
||||
|
||||
@@ -107,3 +107,12 @@ export type RunningModuleItemType = {
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export type AppLogsListItemType = {
|
||||
id: string;
|
||||
source: ChatSchema['source'];
|
||||
time: Date;
|
||||
title: string;
|
||||
messageCount: number;
|
||||
callbackCount: number;
|
||||
};
|
||||
|
3
client/src/types/chat.d.ts
vendored
3
client/src/types/chat.d.ts
vendored
@@ -71,4 +71,7 @@ export type ChatHistoryItemResType = {
|
||||
// content extract
|
||||
extractDescription?: string;
|
||||
extractResult?: Record<string, any>;
|
||||
|
||||
// http
|
||||
httpResult?: Record<string, any>;
|
||||
};
|
||||
|
@@ -6,7 +6,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/imgs/:path*",
|
||||
"destination": "https://cdn.jsdelivr.us/gh/yangchuansheng/FastGPT@main/docSite/assets/imgs/:path*"
|
||||
"destination": "https://cdn.jsdelivr.us/gh/labring/FastGPT@main/docSite/assets/imgs/:path*"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
|
Reference in New Issue
Block a user