Compare commits

..

4 Commits

Author SHA1 Message Date
Archer
917e4e9262 4.8 test fix (#1397)
* adapt v1 chat init

* adapt v1 chat init

* adapt v1 chat init

* perf: message input line; fix: http request un stream

* perf: message input line; fix: http request un stream

* perf: message input line; fix: http request un stream

* perf: error tip
2024-05-08 22:18:22 +08:00
Archer
3c6e5a6e00 4.8 test (#1394)
* fix: chat variable sync

* feat: chat save variable config

* fix: target handle hidden

* adapt v1 chat init

* adapt v1 chat init

* adapt v1 chat init

* adapt v1 chat init
2024-05-08 19:49:17 +08:00
heheer
7b75a99ba2 fix: add pptx encoding try catch (#1393) 2024-05-08 18:10:37 +08:00
heheer
2e468fc8ca 4.8 test fix (#1386)
* fix: boolean of if else input

* fix laf node

* fix laf bind

* fix laf input type

* fix if else check

* fix

* fix
2024-05-08 14:39:02 +08:00
39 changed files with 655 additions and 495 deletions

View File

@@ -2,7 +2,7 @@ import { ErrType } from '../errorCode';
/* dataset: 502000 */ /* dataset: 502000 */
export enum AppErrEnum { export enum AppErrEnum {
unExist = 'unExist', unExist = 'appUnExist',
unAuthApp = 'unAuthApp' unAuthApp = 'unAuthApp'
} }
const appErrList = [ const appErrList = [

View File

@@ -2,8 +2,8 @@ import { ErrType } from '../errorCode';
/* dataset: 506000 */ /* dataset: 506000 */
export enum OpenApiErrEnum { export enum OpenApiErrEnum {
unExist = 'unExist', unExist = 'openapiUnExist',
unAuth = 'unAuth' unAuth = 'openapiUnAuth'
} }
const errList = [ const errList = [
{ {

View File

@@ -2,7 +2,7 @@ import { ErrType } from '../errorCode';
/* dataset: 505000 */ /* dataset: 505000 */
export enum OutLinkErrEnum { export enum OutLinkErrEnum {
unExist = 'unExist', unExist = 'outlinkUnExist',
unAuthLink = 'unAuthLink', unAuthLink = 'unAuthLink',
linkUnInvalid = 'linkUnInvalid', linkUnInvalid = 'linkUnInvalid',

View File

@@ -2,8 +2,8 @@ import { ErrType } from '../errorCode';
/* dataset: 507000 */ /* dataset: 507000 */
export enum PluginErrEnum { export enum PluginErrEnum {
unExist = 'unExist', unExist = 'pluginUnExist',
unAuth = 'unAuth' unAuth = 'pluginUnAuth'
} }
const errList = [ const errList = [
{ {

View File

@@ -105,7 +105,7 @@ export const appWorkflow2Form = ({ nodes }: { nodes: StoreNodeItemType[] }) => {
} else if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) { } else if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) {
const { const {
welcomeText, welcomeText,
variableModules, variableNodes,
questionGuide, questionGuide,
ttsConfig, ttsConfig,
whisperConfig, whisperConfig,
@@ -114,7 +114,7 @@ export const appWorkflow2Form = ({ nodes }: { nodes: StoreNodeItemType[] }) => {
defaultAppForm.userGuide = { defaultAppForm.userGuide = {
welcomeText: welcomeText, welcomeText: welcomeText,
variables: variableModules, variables: variableNodes,
questionGuide: questionGuide, questionGuide: questionGuide,
tts: ttsConfig, tts: ttsConfig,
whisper: whisperConfig, whisper: whisperConfig,

View File

@@ -10,7 +10,7 @@ import {
import { FlowNodeTypeEnum } from '../workflow/node/constant'; import { FlowNodeTypeEnum } from '../workflow/node/constant';
import { NodeOutputKeyEnum } from '../workflow/constants'; import { NodeOutputKeyEnum } from '../workflow/constants';
import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants';
import { AppSchema } from '../app/type'; import { AppSchema, VariableItemType } from '../app/type';
import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d'; import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d';
import { DatasetSearchModeEnum } from '../dataset/constants'; import { DatasetSearchModeEnum } from '../dataset/constants';
import { ChatBoxInputType } from '../../../../projects/app/src/components/ChatBox/type'; import { ChatBoxInputType } from '../../../../projects/app/src/components/ChatBox/type';
@@ -27,11 +27,13 @@ export type ChatSchema = {
title: string; title: string;
customTitle: string; customTitle: string;
top: boolean; top: boolean;
variables: Record<string, any>;
source: `${ChatSourceEnum}`; source: `${ChatSourceEnum}`;
shareId?: string; shareId?: string;
outLinkUid?: string; outLinkUid?: string;
content: ChatItemType[];
variableList?: VariableItemType[];
welcomeText?: string;
variables: Record<string, any>;
metadata?: Record<string, any>; metadata?: Record<string, any>;
}; };

View File

@@ -15,7 +15,7 @@ export const VariableUpdateNode: FlowNodeTemplateType = {
targetHandle: getHandleConfig(true, true, true, true), targetHandle: getHandleConfig(true, true, true, true),
avatar: '/imgs/workflow/variable.png', avatar: '/imgs/workflow/variable.png',
name: '变量更新', name: '变量更新',
intro: '可以更新指定节点的输出值全局变量', intro: '可以更新指定节点的输出值或更新全局变量',
showStatus: true, showStatus: true,
isTool: false, isTool: false,
inputs: [ inputs: [

View File

@@ -36,13 +36,17 @@ export const checkInputIsReference = (input: FlowNodeInputItemType) => {
/* node */ /* node */
export const getGuideModule = (modules: StoreNodeItemType[]) => export const getGuideModule = (modules: StoreNodeItemType[]) =>
modules.find((item) => item.flowNodeType === FlowNodeTypeEnum.systemConfig); modules.find(
(item) =>
item.flowNodeType === FlowNodeTypeEnum.systemConfig ||
// @ts-ignore (adapt v1)
item.flowType === FlowNodeTypeEnum.systemConfig
);
export const splitGuideModule = (guideModules?: StoreNodeItemType) => { export const splitGuideModule = (guideModules?: StoreNodeItemType) => {
const welcomeText: string = const welcomeText: string =
guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.welcomeText)?.value || ''; guideModules?.inputs?.find((item) => item.key === NodeInputKeyEnum.welcomeText)?.value || '';
const variableModules: VariableItemType[] = const variableNodes: VariableItemType[] =
guideModules?.inputs.find((item) => item.key === NodeInputKeyEnum.variables)?.value || []; guideModules?.inputs.find((item) => item.key === NodeInputKeyEnum.variables)?.value || [];
const questionGuide: boolean = const questionGuide: boolean =
@@ -63,13 +67,43 @@ export const splitGuideModule = (guideModules?: StoreNodeItemType) => {
return { return {
welcomeText, welcomeText,
variableModules, variableNodes,
questionGuide, questionGuide,
ttsConfig, ttsConfig,
whisperConfig, whisperConfig,
scheduledTriggerConfig scheduledTriggerConfig
}; };
}; };
export const replaceAppChatConfig = ({
node,
variableList,
welcomeText
}: {
node?: StoreNodeItemType;
variableList?: VariableItemType[];
welcomeText?: string;
}): StoreNodeItemType | undefined => {
if (!node) return;
return {
...node,
inputs: node.inputs.map((input) => {
if (input.key === NodeInputKeyEnum.variables && variableList) {
return {
...input,
value: variableList
};
}
if (input.key === NodeInputKeyEnum.welcomeText && welcomeText) {
return {
...input,
value: welcomeText
};
}
return input;
})
};
};
export const getOrInitModuleInputValue = (input: FlowNodeInputItemType) => { export const getOrInitModuleInputValue = (input: FlowNodeInputItemType) => {
if (input.value !== undefined || !input.valueType) return input.value; if (input.value !== undefined || !input.valueType) return input.value;

View File

@@ -28,6 +28,6 @@ try {
console.log(error); console.log(error);
} }
export const MongoRwaTextBuffer: Model<RawTextBufferSchemaType> = export const MongoRawTextBuffer: Model<RawTextBufferSchemaType> =
models[collectionName] || model(collectionName, RawTextBufferSchema); models[collectionName] || model(collectionName, RawTextBufferSchema);
MongoRwaTextBuffer.syncIndexes(); MongoRawTextBuffer.syncIndexes();

View File

@@ -6,7 +6,7 @@ import { DatasetFileSchema } from '@fastgpt/global/core/dataset/type';
import { MongoFileSchema } from './schema'; import { MongoFileSchema } from './schema';
import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoRwaTextBuffer } from '../../buffer/rawText/schema'; import { MongoRawTextBuffer } from '../../buffer/rawText/schema';
import { readFileRawContent } from '../read/utils'; import { readFileRawContent } from '../read/utils';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
@@ -162,7 +162,7 @@ export const readFileContentFromMongo = async ({
filename: string; filename: string;
}> => { }> => {
// read buffer // read buffer
const fileBuffer = await MongoRwaTextBuffer.findOne({ sourceId: fileId }).lean(); const fileBuffer = await MongoRawTextBuffer.findOne({ sourceId: fileId }).lean();
if (fileBuffer) { if (fileBuffer) {
return { return {
rawText: fileBuffer.rawText, rawText: fileBuffer.rawText,
@@ -208,7 +208,7 @@ export const readFileContentFromMongo = async ({
}); });
if (rawText.trim()) { if (rawText.trim()) {
MongoRwaTextBuffer.create({ MongoRawTextBuffer.create({
sourceId: fileId, sourceId: fileId,
rawText, rawText,
metadata: { metadata: {

View File

@@ -41,6 +41,10 @@ export function reRankRecall({
.then((data) => { .then((data) => {
addLog.info('ReRank finish:', { time: Date.now() - start }); addLog.info('ReRank finish:', { time: Date.now() - start });
if (!data?.results || data?.results?.length === 0) {
addLog.error('ReRank error, empty result', data);
}
return data?.results?.map((item) => ({ return data?.results?.map((item) => ({
id: documents[item.index].id, id: documents[item.index].id,
score: item.relevance_score score: item.relevance_score

View File

@@ -61,7 +61,15 @@ const ChatSchema = new Schema({
outLinkUid: { outLinkUid: {
type: String type: String
}, },
variableList: {
type: Array
},
welcomeText: {
type: String
},
variables: { variables: {
// variable value
type: Object, type: Object,
default: {} default: {}
}, },

View File

@@ -290,7 +290,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
runtimeNodes, runtimeNodes,
runtimeEdges, runtimeEdges,
params, params,
mode: 'test' mode: props.mode === 'debug' ? 'test' : props.mode
}; };
// run module // run module

View File

@@ -45,6 +45,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
detail, detail,
appId, appId,
chatId, chatId,
stream,
responseChatItemId, responseChatItemId,
variables, variables,
node: { outputs }, node: { outputs },
@@ -132,7 +133,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
results[key] = valueTypeFormat(formatResponse[key], output.valueType); results[key] = valueTypeFormat(formatResponse[key], output.valueType);
} }
if (typeof formatResponse[NodeOutputKeyEnum.answerText] === 'string') { if (stream && typeof formatResponse[NodeOutputKeyEnum.answerText] === 'string') {
responseWrite({ responseWrite({
res, res,
event: detail ? SseResponseEventEnum.fastAnswer : undefined, event: detail ? SseResponseEventEnum.fastAnswer : undefined,

View File

@@ -22,20 +22,55 @@ type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.ifElseResult]: string; [NodeOutputKeyEnum.ifElseResult]: string;
}>; }>;
function isEmpty(value: any) {
return (
// 检查未定义或null值
value === undefined ||
value === null ||
// 检查空字符串
(typeof value === 'string' && value.trim() === '') ||
// 检查NaN
(typeof value === 'number' && isNaN(value)) ||
// 检查空数组
(Array.isArray(value) && value.length === 0) ||
// 检查空对象
(typeof value === 'object' && Object.keys(value).length === 0)
);
}
function isInclude(value: any, target: any) {
if (Array.isArray(value)) {
return value.map((item: any) => String(item)).includes(target);
} else if (typeof value === 'string') {
return value.includes(target);
} else {
return false;
}
}
function checkCondition(condition: VariableConditionEnum, variableValue: any, value: string) { function checkCondition(condition: VariableConditionEnum, variableValue: any, value: string) {
const operations = { const operations = {
[VariableConditionEnum.isEmpty]: () => !variableValue, [VariableConditionEnum.isEmpty]: () => isEmpty(variableValue),
[VariableConditionEnum.isNotEmpty]: () => !!variableValue, [VariableConditionEnum.isNotEmpty]: () => !isEmpty(variableValue),
[VariableConditionEnum.equalTo]: () => variableValue === value,
[VariableConditionEnum.notEqual]: () => variableValue !== value, [VariableConditionEnum.equalTo]: () => String(variableValue) === value,
[VariableConditionEnum.notEqual]: () => String(variableValue) !== value,
// number
[VariableConditionEnum.greaterThan]: () => Number(variableValue) > Number(value), [VariableConditionEnum.greaterThan]: () => Number(variableValue) > Number(value),
[VariableConditionEnum.lessThan]: () => Number(variableValue) < Number(value), [VariableConditionEnum.lessThan]: () => Number(variableValue) < Number(value),
[VariableConditionEnum.greaterThanOrEqualTo]: () => Number(variableValue) >= Number(value), [VariableConditionEnum.greaterThanOrEqualTo]: () => Number(variableValue) >= Number(value),
[VariableConditionEnum.lessThanOrEqualTo]: () => Number(variableValue) <= Number(value), [VariableConditionEnum.lessThanOrEqualTo]: () => Number(variableValue) <= Number(value),
[VariableConditionEnum.include]: () => variableValue?.includes(value),
[VariableConditionEnum.notInclude]: () => !variableValue?.includes(value), // array or string
[VariableConditionEnum.include]: () => isInclude(variableValue, value),
[VariableConditionEnum.notInclude]: () => !isInclude(variableValue, value),
// string
[VariableConditionEnum.startWith]: () => variableValue?.startsWith(value), [VariableConditionEnum.startWith]: () => variableValue?.startsWith(value),
[VariableConditionEnum.endWith]: () => variableValue?.endsWith(value), [VariableConditionEnum.endWith]: () => variableValue?.endsWith(value),
// array
[VariableConditionEnum.lengthEqualTo]: () => variableValue?.length === Number(value), [VariableConditionEnum.lengthEqualTo]: () => variableValue?.length === Number(value),
[VariableConditionEnum.lengthNotEqualTo]: () => variableValue?.length !== Number(value), [VariableConditionEnum.lengthNotEqualTo]: () => variableValue?.length !== Number(value),
[VariableConditionEnum.lengthGreaterThan]: () => variableValue?.length > Number(value), [VariableConditionEnum.lengthGreaterThan]: () => variableValue?.length > Number(value),

View File

@@ -44,9 +44,13 @@ const parsePowerPoint = async ({
} }
// Returning an array of all the xml contents read using fs.readFileSync // Returning an array of all the xml contents read using fs.readFileSync
const xmlContentArray = files.map((file) => const xmlContentArray = files.map((file) => {
fs.readFileSync(`${decompressPath}/${file.path}`, encoding) try {
); return fs.readFileSync(`${decompressPath}/${file.path}`, encoding);
} catch (err) {
return fs.readFileSync(`${decompressPath}/${file.path}`, 'utf-8');
}
});
let responseArr: string[] = []; let responseArr: string[] = [];
@@ -95,9 +99,15 @@ export const parseOffice = async ({
// const decompressPath = `${DEFAULTDECOMPRESSSUBLOCATION}/test`; // const decompressPath = `${DEFAULTDECOMPRESSSUBLOCATION}/test`;
// write new file // write new file
fs.writeFileSync(filepath, buffer, { try {
encoding fs.writeFileSync(filepath, buffer, {
}); encoding
});
} catch (err) {
fs.writeFileSync(filepath, buffer, {
encoding: 'utf-8'
});
}
const text = await (async () => { const text = await (async () => {
try { try {

View File

@@ -85,7 +85,22 @@
"x": 1607.7142331269126, "x": 1607.7142331269126,
"y": -151.8669210746189 "y": -151.8669210746189
}, },
"inputs": [], "inputs": [
{
"key": "text",
"valueType": "string",
"label": "text",
"renderTypeList": ["reference"],
"description": "",
"canEdit": true,
"editField": {
"key": true,
"description": true,
"valueType": true
},
"value": ["CRT7oIEU8v2P", "pYKS0LB9gAr3"]
}
],
"outputs": [] "outputs": []
}, },
{ {
@@ -190,6 +205,13 @@
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。", "description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any", "valueType": "any",
"type": "static" "type": "static"
},
{
"id": "pYKS0LB9gAr3",
"type": "dynamic",
"key": "text",
"valueType": "string",
"label": "text"
} }
] ]
} }

View File

@@ -357,9 +357,16 @@ const MessageInput = ({
// enter send.(pc or iframe && enter and unPress shift) // enter send.(pc or iframe && enter and unPress shift)
const isEnter = e.keyCode === 13; const isEnter = e.keyCode === 13;
if (isEnter && TextareaDom.current && (e.ctrlKey || e.altKey)) { if (isEnter && TextareaDom.current && (e.ctrlKey || e.altKey)) {
TextareaDom.current.value += '\n'; // Add a new line
const index = TextareaDom.current.selectionStart;
const val = TextareaDom.current.value;
TextareaDom.current.value = `${val.slice(0, index)}\n${val.slice(index)}`;
TextareaDom.current.selectionStart = index + 1;
TextareaDom.current.selectionEnd = index + 1;
TextareaDom.current.style.height = textareaMinH; TextareaDom.current.style.height = textareaMinH;
TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight}px`; TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight}px`;
return; return;
} }

View File

@@ -12,7 +12,7 @@ import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
type useChatStoreType = OutLinkChatAuthProps & { type useChatStoreType = OutLinkChatAuthProps & {
welcomeText: string; welcomeText: string;
variableModules: VariableItemType[]; variableNodes: VariableItemType[];
questionGuide: boolean; questionGuide: boolean;
ttsConfig: AppTTSConfigType; ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType; whisperConfig: AppWhisperConfigType;
@@ -41,7 +41,7 @@ type useChatStoreType = OutLinkChatAuthProps & {
}; };
const StateContext = createContext<useChatStoreType>({ const StateContext = createContext<useChatStoreType>({
welcomeText: '', welcomeText: '',
variableModules: [], variableNodes: [],
questionGuide: false, questionGuide: false,
ttsConfig: { ttsConfig: {
type: 'none', type: 'none',
@@ -110,7 +110,7 @@ const Provider = ({
}: ChatProviderProps) => { }: ChatProviderProps) => {
const [chatHistories, setChatHistories] = useState<ChatSiteItemType[]>([]); const [chatHistories, setChatHistories] = useState<ChatSiteItemType[]>([]);
const { welcomeText, variableModules, questionGuide, ttsConfig, whisperConfig } = useMemo( const { welcomeText, variableNodes, questionGuide, ttsConfig, whisperConfig } = useMemo(
() => splitGuideModule(userGuideModule), () => splitGuideModule(userGuideModule),
[userGuideModule] [userGuideModule]
); );
@@ -150,7 +150,7 @@ const Provider = ({
teamId, teamId,
teamToken, teamToken,
welcomeText, welcomeText,
variableModules, variableNodes,
questionGuide, questionGuide,
ttsConfig, ttsConfig,
whisperConfig, whisperConfig,

View File

@@ -1,5 +1,5 @@
import { VariableItemType } from '@fastgpt/global/core/app/type.d'; import { VariableItemType } from '@fastgpt/global/core/app/type.d';
import React, { useEffect, useState } from 'react'; import React from 'react';
import { UseFormReturn } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react'; import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react';
@@ -12,34 +12,19 @@ import { ChatBoxInputFormType } from '../type.d';
const VariableInput = ({ const VariableInput = ({
appAvatar, appAvatar,
variableModules, variableNodes,
variableIsFinish,
chatForm, chatForm,
onSubmitVariables onSubmitVariables
}: { }: {
appAvatar?: string; appAvatar?: string;
variableModules: VariableItemType[]; variableNodes: VariableItemType[];
variableIsFinish: boolean;
onSubmitVariables: (e: Record<string, any>) => void; onSubmitVariables: (e: Record<string, any>) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>; chatForm: UseFormReturn<ChatBoxInputFormType>;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { register, unregister, setValue, handleSubmit: handleSubmitChat, watch } = chatForm; const { register, setValue, handleSubmit: handleSubmitChat, watch } = chatForm;
const variables = watch('variables'); const variables = watch('variables');
const chatStarted = watch('chatStarted');
useEffect(() => {
// 重新注册所有字段
variableModules.forEach((item) => {
register(`variables.${item.key}`, { required: item.required });
});
return () => {
// 组件卸载时注销所有字段
variableModules.forEach((item) => {
unregister(`variables.${item.key}`);
});
};
}, [register, unregister, variableModules]);
return ( return (
<Box py={3}> <Box py={3}>
@@ -55,7 +40,7 @@ const VariableInput = ({
bg={'white'} bg={'white'}
boxShadow={'0 0 8px rgba(0,0,0,0.15)'} boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
> >
{variableModules.map((item) => ( {variableNodes.map((item) => (
<Box key={item.id} mb={4}> <Box key={item.id} mb={4}>
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}> <Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
{item.label} {item.label}
@@ -73,7 +58,6 @@ const VariableInput = ({
</Box> </Box>
{item.type === VariableInputEnum.input && ( {item.type === VariableInputEnum.input && (
<Input <Input
isDisabled={variableIsFinish}
bg={'myWhite.400'} bg={'myWhite.400'}
{...register(`variables.${item.key}`, { {...register(`variables.${item.key}`, {
required: item.required required: item.required
@@ -82,7 +66,6 @@ const VariableInput = ({
)} )}
{item.type === VariableInputEnum.textarea && ( {item.type === VariableInputEnum.textarea && (
<Textarea <Textarea
isDisabled={variableIsFinish}
bg={'myWhite.400'} bg={'myWhite.400'}
{...register(`variables.${item.key}`, { {...register(`variables.${item.key}`, {
required: item.required required: item.required
@@ -94,7 +77,6 @@ const VariableInput = ({
{item.type === VariableInputEnum.select && ( {item.type === VariableInputEnum.select && (
<MySelect <MySelect
width={'100%'} width={'100%'}
isDisabled={variableIsFinish}
list={(item.enums || []).map((item) => ({ list={(item.enums || []).map((item) => ({
label: item.value, label: item.value,
value: item.value value: item.value
@@ -110,7 +92,7 @@ const VariableInput = ({
)} )}
</Box> </Box>
))} ))}
{!variableIsFinish && ( {!chatStarted && (
<Button <Button
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />} leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
size={'sm'} size={'sm'}

View File

@@ -58,6 +58,8 @@ import ChatProvider, { useChatProviderStore } from './Provider';
import ChatItem from './components/ChatItem'; import ChatItem from './components/ChatItem';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useCreation, useUpdateEffect } from 'ahooks';
const ResponseTags = dynamic(() => import('./ResponseTags')); const ResponseTags = dynamic(() => import('./ResponseTags'));
const FeedbackModal = dynamic(() => import('./FeedbackModal')); const FeedbackModal = dynamic(() => import('./FeedbackModal'));
const ReadFeedbackModal = dynamic(() => import('./ReadFeedbackModal')); const ReadFeedbackModal = dynamic(() => import('./ReadFeedbackModal'));
@@ -147,7 +149,7 @@ const ChatBox = (
const { const {
welcomeText, welcomeText,
variableModules, variableNodes,
questionGuide, questionGuide,
startSegmentedAudio, startSegmentedAudio,
finishSegmentedAudio, finishSegmentedAudio,
@@ -171,24 +173,10 @@ const ChatBox = (
const chatStarted = watch('chatStarted'); const chatStarted = watch('chatStarted');
/* variable */ /* variable */
const variables = watch('variables'); const filterVariableNodes = useCreation(
const filterVariableModules = useMemo( () => variableNodes.filter((item) => item.type !== VariableInputEnum.custom),
() => variableModules.filter((item) => item.type !== VariableInputEnum.custom), [variableNodes]
[variableModules]
); );
const variableIsFinish = (() => {
if (!filterVariableModules || filterVariableModules.length === 0 || chatHistories.length > 0)
return true;
for (let i = 0; i < filterVariableModules.length; i++) {
const item = filterVariableModules[i];
if (item.required && !variables[item.key]) {
return false;
}
}
return chatStarted;
})();
// 滚动到底部 // 滚动到底部
const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => { const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => {
@@ -379,174 +367,185 @@ const ChatBox = (
autoTTSResponse?: boolean; autoTTSResponse?: boolean;
history?: ChatSiteItemType[]; history?: ChatSiteItemType[];
}) => { }) => {
handleSubmit(async ({ variables }) => { handleSubmit(
if (!onStartChat) return; async ({ variables }) => {
if (isChatting) { if (!onStartChat) return;
toast({ if (isChatting) {
title: '正在聊天中...请等待结束', toast({
status: 'warning' title: '正在聊天中...请等待结束',
}); status: 'warning'
return;
}
abortRequest();
text = text.trim();
if (!text && files.length === 0) {
toast({
title: '内容为空',
status: 'warning'
});
return;
}
const responseChatId = getNanoid(24);
questionGuideController.current?.abort('stop');
// set auto audio playing
if (autoTTSResponse) {
await startSegmentedAudio();
setAudioPlayingChatId(responseChatId);
}
const newChatList: ChatSiteItemType[] = [
...history,
{
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
value: [
...files.map((file) => ({
type: ChatItemValueTypeEnum.file,
file: {
type: file.type,
name: file.name,
url: file.url || ''
}
})),
...(text
? [
{
type: ChatItemValueTypeEnum.text,
text: {
content: text
}
}
]
: [])
] as UserChatItemValueItemType[],
status: 'finish'
},
{
dataId: responseChatId,
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: ''
}
}
],
status: 'loading'
}
];
// 插入内容
setChatHistories(newChatList);
// 清空输入内容
resetInputVal({});
setQuestionGuide([]);
setTimeout(() => {
scrollToBottom();
}, 100);
try {
// create abort obj
const abortSignal = new AbortController();
chatController.current = abortSignal;
const messages = chats2GPTMessages({ messages: newChatList, reserveId: true });
const {
responseData,
responseText,
newVariables,
isNewChat = false
} = await onStartChat({
chatList: newChatList,
messages,
controller: abortSignal,
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
variables
});
newVariables && setValue('variables', newVariables);
isNewChatReplace.current = isNewChat;
// set finish status
setChatHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish',
responseData
};
})
);
setTimeout(() => {
createQuestionGuide({
history: newChatList.map((item, i) =>
i === newChatList.length - 1
? {
...item,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: responseText
}
}
]
}
: item
)
}); });
generatingScroll(); return;
isPc && TextareaDom.current?.focus();
}, 100);
// tts audio
autoTTSResponse && splitText2Audio(responseText, true);
} catch (err: any) {
toast({
title: t(getErrText(err, 'core.chat.error.Chat error')),
status: 'error',
duration: 5000,
isClosable: true
});
if (!err?.responseText) {
resetInputVal({ text, files });
setChatHistories(newChatList.slice(0, newChatList.length - 2));
} }
// set finish status abortRequest();
setChatHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
}
autoTTSResponse && finishSegmentedAudio(); text = text.trim();
})();
if (!text && files.length === 0) {
toast({
title: '内容为空',
status: 'warning'
});
return;
}
// delete invalid variables 只保留在 variableNodes 中的变量
const requestVariables: Record<string, any> = {};
variableNodes?.forEach((item) => {
requestVariables[item.key] = variables[item.key] || '';
});
const responseChatId = getNanoid(24);
questionGuideController.current?.abort('stop');
// set auto audio playing
if (autoTTSResponse) {
await startSegmentedAudio();
setAudioPlayingChatId(responseChatId);
}
const newChatList: ChatSiteItemType[] = [
...history,
{
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
value: [
...files.map((file) => ({
type: ChatItemValueTypeEnum.file,
file: {
type: file.type,
name: file.name,
url: file.url || ''
}
})),
...(text
? [
{
type: ChatItemValueTypeEnum.text,
text: {
content: text
}
}
]
: [])
] as UserChatItemValueItemType[],
status: 'finish'
},
{
dataId: responseChatId,
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: ''
}
}
],
status: 'loading'
}
];
// 插入内容
setChatHistories(newChatList);
// 清空输入内容
resetInputVal({});
setQuestionGuide([]);
setTimeout(() => {
scrollToBottom();
}, 100);
try {
// create abort obj
const abortSignal = new AbortController();
chatController.current = abortSignal;
const messages = chats2GPTMessages({ messages: newChatList, reserveId: true });
const {
responseData,
responseText,
newVariables,
isNewChat = false
} = await onStartChat({
chatList: newChatList,
messages,
controller: abortSignal,
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
variables: requestVariables
});
newVariables && setValue('variables', newVariables);
isNewChatReplace.current = isNewChat;
// set finish status
setChatHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish',
responseData
};
})
);
setTimeout(() => {
createQuestionGuide({
history: newChatList.map((item, i) =>
i === newChatList.length - 1
? {
...item,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: responseText
}
}
]
}
: item
)
});
generatingScroll();
isPc && TextareaDom.current?.focus();
}, 100);
// tts audio
autoTTSResponse && splitText2Audio(responseText, true);
} catch (err: any) {
toast({
title: t(getErrText(err, 'core.chat.error.Chat error')),
status: 'error',
duration: 5000,
isClosable: true
});
if (!err?.responseText) {
resetInputVal({ text, files });
setChatHistories(newChatList.slice(0, newChatList.length - 2));
}
// set finish status
setChatHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
}
autoTTSResponse && finishSegmentedAudio();
},
(err) => {
console.log(err?.variables);
}
)();
}, },
[ [
abortRequest, abortRequest,
@@ -566,7 +565,8 @@ const ChatBox = (
splitText2Audio, splitText2Audio,
startSegmentedAudio, startSegmentedAudio,
t, t,
toast toast,
variableNodes
] ]
); );
@@ -630,7 +630,7 @@ const ChatBox = (
}); });
}; };
}, },
[onDelMessage] [onDelMessage, setChatHistories]
); );
// admin mark // admin mark
const onMark = useCallback( const onMark = useCallback(
@@ -796,7 +796,19 @@ const ChatBox = (
} }
}; };
}, },
[appId, chatId] [appId, chatId, setChatHistories]
);
const resetVariables = useCallback(
(e: Record<string, any> = {}) => {
const value: Record<string, any> = { ...e };
filterVariableNodes?.forEach((item) => {
value[item.key] = e[item.key] || '';
});
setValue('variables', value);
},
[filterVariableNodes, setValue]
); );
const showEmpty = useMemo( const showEmpty = useMemo(
@@ -804,13 +816,13 @@ const ChatBox = (
feConfigs?.show_emptyChat && feConfigs?.show_emptyChat &&
showEmptyIntro && showEmptyIntro &&
chatHistories.length === 0 && chatHistories.length === 0 &&
!filterVariableModules?.length && !filterVariableNodes?.length &&
!welcomeText, !welcomeText,
[ [
chatHistories.length, chatHistories.length,
feConfigs?.show_emptyChat, feConfigs?.show_emptyChat,
showEmptyIntro, showEmptyIntro,
filterVariableModules?.length, filterVariableNodes?.length,
welcomeText welcomeText
] ]
); );
@@ -869,14 +881,7 @@ const ChatBox = (
// output data // output data
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getChatHistories: () => chatHistories, getChatHistories: () => chatHistories,
resetVariables(e) { resetVariables,
const defaultVal: Record<string, any> = {};
filterVariableModules?.forEach((item) => {
defaultVal[item.key] = '';
});
setValue('variables', e || defaultVal);
},
resetHistory(e) { resetHistory(e) {
abortRequest(); abortRequest();
setValue('chatStarted', e.length > 0); setValue('chatStarted', e.length > 0);
@@ -891,7 +896,7 @@ const ChatBox = (
})); }));
return ( return (
<Flex flexDirection={'column'} h={'100%'}> <Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script> <Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
{/* chat box container */} {/* chat box container */}
<Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}> <Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}>
@@ -899,11 +904,10 @@ const ChatBox = (
{showEmpty && <Empty />} {showEmpty && <Empty />}
{!!welcomeText && <WelcomeBox appAvatar={appAvatar} welcomeText={welcomeText} />} {!!welcomeText && <WelcomeBox appAvatar={appAvatar} welcomeText={welcomeText} />}
{/* variable input */} {/* variable input */}
{!!filterVariableModules?.length && ( {!!filterVariableNodes?.length && (
<VariableInput <VariableInput
appAvatar={appAvatar} appAvatar={appAvatar}
variableModules={filterVariableModules} variableNodes={filterVariableNodes}
variableIsFinish={variableIsFinish}
chatForm={chatForm} chatForm={chatForm}
onSubmitVariables={(data) => { onSubmitVariables={(data) => {
setValue('chatStarted', true); setValue('chatStarted', true);
@@ -995,7 +999,7 @@ const ChatBox = (
</Box> </Box>
</Box> </Box>
{/* message input */} {/* message input */}
{onStartChat && variableIsFinish && active && ( {onStartChat && (chatStarted || filterVariableNodes.length === 0) && active && (
<MessageInput <MessageInput
onSendMessage={sendPrompt} onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')} onStop={() => chatController.current?.abort('stop')}

View File

@@ -34,11 +34,13 @@ export type ChatTestComponentRef = {
const ChatTest = ( const ChatTest = (
{ {
app, app,
isOpen,
nodes = [], nodes = [],
edges = [], edges = [],
onClose onClose
}: { }: {
app: AppSchema; app: AppSchema;
isOpen: boolean;
nodes?: StoreNodeItemType[]; nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[]; edges?: StoreEdgeItemType[];
onClose: () => void; onClose: () => void;
@@ -48,7 +50,6 @@ const ChatTest = (
const { t } = useTranslation(); const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null); const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const isOpen = useMemo(() => nodes && nodes.length > 0, [nodes]);
const startChat = useCallback( const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => { async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {

View File

@@ -17,6 +17,7 @@ import {
arrayConditionList, arrayConditionList,
booleanConditionList, booleanConditionList,
numberConditionList, numberConditionList,
objectConditionList,
stringConditionList stringConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant'; } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
@@ -133,7 +134,8 @@ const ListItem = ({
if (index === i) { if (index === i) {
return { return {
...item, ...item,
variable: e variable: e,
condition: undefined
}; };
} }
return item; return item;
@@ -334,10 +336,10 @@ const ConditionSelect = ({
valueType === WorkflowIOValueTypeEnum.arrayBoolean || valueType === WorkflowIOValueTypeEnum.arrayBoolean ||
valueType === WorkflowIOValueTypeEnum.arrayNumber || valueType === WorkflowIOValueTypeEnum.arrayNumber ||
valueType === WorkflowIOValueTypeEnum.arrayObject || valueType === WorkflowIOValueTypeEnum.arrayObject ||
valueType === WorkflowIOValueTypeEnum.arrayString || valueType === WorkflowIOValueTypeEnum.arrayString
valueType === WorkflowIOValueTypeEnum.object
) )
return arrayConditionList; return arrayConditionList;
if (valueType === WorkflowIOValueTypeEnum.object) return objectConditionList;
if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList; if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList;
@@ -395,6 +397,10 @@ const ConditionValueInput = ({
onchange={onChange} onchange={onChange}
value={value} value={value}
placeholder={'选择值'} placeholder={'选择值'}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
/> />
); );
} else { } else {

View File

@@ -33,6 +33,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import IOTitle from '../components/IOTitle'; import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; import { WorkflowContext } from '../../context';
import { putUpdateTeam } from '@/web/support/user/team/api';
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal')); const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
@@ -47,20 +48,11 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl); const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const { userInfo } = useUserStore(); const { userInfo, initUserInfo } = useUserStore();
const token = userInfo?.team.lafAccount?.token; const token = userInfo?.team.lafAccount?.token;
const appid = userInfo?.team.lafAccount?.appid; const appid = userInfo?.team.lafAccount?.appid;
// not config laf
if (!token || !appid) {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<ConfigLaf />
</NodeCard>
);
}
const { const {
data: lafData, data: lafData,
isLoading: isLoadingFunctions, isLoading: isLoadingFunctions,
@@ -69,24 +61,32 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
['getLafFunctionList'], ['getLafFunctionList'],
async () => { async () => {
// load laf app detail // load laf app detail
const appDetail = await getLafAppDetail(appid); try {
const appDetail = await getLafAppDetail(appid || '');
// load laf app functions
const schemaUrl = `https://${appDetail?.domain.domain}/_/api-docs?token=${appDetail?.openapi_token}`;
// load laf app functions const schema = await getApiSchemaByUrl(schemaUrl);
const schemaUrl = `https://${appDetail?.domain.domain}/_/api-docs?token=${appDetail?.openapi_token}`; const openApiSchema = await str2OpenApiSchema(JSON.stringify(schema));
const filterPostSchema = openApiSchema.pathData.filter((item) => item.method === 'post');
const schema = await getApiSchemaByUrl(schemaUrl); return {
const openApiSchema = await str2OpenApiSchema(JSON.stringify(schema)); lafApp: appDetail,
const filterPostSchema = openApiSchema.pathData.filter((item) => item.method === 'post'); lafFunctions: filterPostSchema.map((item) => ({
...item,
return { requestUrl: `https://${appDetail?.domain.domain}${item.path}`
lafApp: appDetail, }))
lafFunctions: filterPostSchema.map((item) => ({ };
...item, } catch (err) {
requestUrl: `https://${appDetail?.domain.domain}${item.path}` await putUpdateTeam({
})) teamId: userInfo?.team.teamId || '',
}; lafAccount: { token: '', appid: '', pat: '' }
});
initUserInfo();
}
}, },
{ {
enabled: !!token && !!appid,
onError(err) { onError(err) {
toast({ toast({
status: 'error', status: 'error',
@@ -155,14 +155,14 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
desc: bodyParams[key].description, desc: bodyParams[key].description,
required: requiredParams?.includes(key) || false, required: requiredParams?.includes(key) || false,
value: `{{${key}}}`, value: `{{${key}}}`,
type: 'string' type: bodyParams[key].type
})) }))
].filter((item) => !inputs.find((input) => input.key === item.name)); ].filter((item) => !inputs.find((input) => input.key === item.name));
allParams.forEach((param) => { allParams.forEach((param) => {
const newInput: FlowNodeInputItemType = { const newInput: FlowNodeInputItemType = {
key: param.name, key: param.name,
valueType: WorkflowIOValueTypeEnum.string, valueType: param.type,
label: param.name, label: param.name,
renderTypeList: [FlowNodeInputTypeEnum.reference], renderTypeList: [FlowNodeInputTypeEnum.reference],
required: param.required, required: param.required,
@@ -215,54 +215,63 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
successToast: t('common.Sync success') successToast: t('common.Sync success')
}); });
return ( // not config laf
<NodeCard minW={'350px'} selected={selected} {...data}> if (!token || !appid) {
<Container> return (
{/* select function */} <NodeCard minW={'350px'} selected={selected} {...data}>
<MySelect <ConfigLaf />
isLoading={isLoadingFunctions} </NodeCard>
list={lafFunctionSelectList} );
placeholder={t('core.module.laf.Select laf function')} } else {
onchange={(e) => { return (
onChangeNode({ <NodeCard minW={'350px'} selected={selected} {...data}>
nodeId, <Container>
type: 'updateInput', {/* select function */}
key: NodeInputKeyEnum.httpReqUrl, <MySelect
value: { isLoading={isLoadingFunctions}
...requestUrl, list={lafFunctionSelectList}
value: e placeholder={t('core.module.laf.Select laf function')}
} onchange={(e) => {
}); onChangeNode({
}} nodeId,
value={selectedFunction} type: 'updateInput',
/> key: NodeInputKeyEnum.httpReqUrl,
{/* auto set params and go to edit */} value: {
{!!selectedFunction && ( ...requestUrl,
<Flex justifyContent={'flex-end'} mt={2} gap={2}> value: e
<Button isLoading={isSyncing} variant={'grayBase'} size={'sm'} onClick={onSyncParams}> }
{t('core.module.Laf sync params')} });
</Button> }}
<Button value={selectedFunction}
variant={'grayBase'} />
size={'sm'} {/* auto set params and go to edit */}
onClick={() => { {!!selectedFunction && (
const lafFunction = lafData?.lafFunctions.find( <Flex justifyContent={'flex-end'} mt={2} gap={2}>
(item) => item.requestUrl === selectedFunction <Button isLoading={isSyncing} variant={'grayBase'} size={'sm'} onClick={onSyncParams}>
); {t('core.module.Laf sync params')}
</Button>
<Button
variant={'grayBase'}
size={'sm'}
onClick={() => {
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
if (!lafFunction) return; if (!lafFunction) return;
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`; const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
window.open(url, '_blank'); window.open(url, '_blank');
}} }}
> >
{t('plugin.go to laf')} {t('plugin.go to laf')}
</Button> </Button>
</Flex> </Flex>
)} )}
</Container> </Container>
{!!selectedFunction && <RenderIO {...props} />} {!!selectedFunction && <RenderIO {...props} />}
</NodeCard> </NodeCard>
); );
}
}; };
export default React.memo(NodeLaf); export default React.memo(NodeLaf);

View File

@@ -28,7 +28,7 @@ export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
edges.some((edge) => edge.targetHandle === getHandleId(nodeId, 'target', 'top'))); edges.some((edge) => edge.targetHandle === getHandleId(nodeId, 'target', 'top')));
const Render = useMemo(() => { const Render = useMemo(() => {
return ( return hidden ? null : (
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}> <MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle <Handle
style={{ style={{
@@ -49,7 +49,7 @@ export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
border={'4px solid #8774EE'} border={'4px solid #8774EE'}
transform={'translate(0,-30%) rotate(45deg)'} transform={'translate(0,-30%) rotate(45deg)'}
pointerEvents={'none'} pointerEvents={'none'}
visibility={hidden ? 'hidden' : 'visible'} visibility={'visible'}
/> />
</Handle> </Handle>
</MyTooltip> </MyTooltip>

View File

@@ -200,14 +200,19 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
) { ) {
return false; return false;
} }
if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false; if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false;
// Same source node // From same source node
if (connectedEdges.some((item) => item.target === nodeId && item.targetHandle !== handleId)) if (
connectedEdges.some(
(item) => item.source === connectingEdge?.nodeId && item.target === nodeId
)
)
return false; return false;
return true; return true;
}, [connectedEdges, connectingEdge?.handleId, edges, handleId, node, nodeId]); }, [connectedEdges, connectingEdge?.handleId, connectingEdge?.nodeId, edges, node, nodeId]);
const RenderHandle = useMemo(() => { const RenderHandle = useMemo(() => {
return ( return (

View File

@@ -487,7 +487,6 @@ const WorkflowContextProvider = ({
// 3. Set entry node status to running // 3. Set entry node status to running
entryNodes.forEach((node) => { entryNodes.forEach((node) => {
if (runtimeNodeStatus[node.nodeId] !== 'wait') { if (runtimeNodeStatus[node.nodeId] !== 'wait') {
console.log(node.name);
onChangeNode({ onChangeNode({
nodeId: node.nodeId, nodeId: node.nodeId,
type: 'attr', type: 'attr',

View File

@@ -67,7 +67,7 @@ const LafAccountModal = ({
enabled: !!lafToken, enabled: !!lafToken,
onSuccess: (data) => { onSuccess: (data) => {
if (!getValues('appid') && data.length > 0) { if (!getValues('appid') && data.length > 0) {
setValue('appid', data[0].appid); setValue('appid', data.filter((app) => app.state === 'Running')[0]?.appid);
} }
}, },
onError: (err) => { onError: (err) => {
@@ -175,7 +175,13 @@ const LafAccountModal = ({
)} )}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}> <Button
variant={'whiteBase'}
onClick={() => {
initUserInfo();
onClose();
}}
>
{t('common.Close')} {t('common.Close')}
</Button> </Button>
{appid && ( {appid && (

View File

@@ -1,8 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils'; import { getGuideModule, replaceAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow'; import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d'; import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
@@ -10,74 +9,73 @@ import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(
try { req: NextApiRequest,
await connectToDatabase(); res: NextApiResponse
): Promise<InitChatResponse | void> {
let { appId, chatId, loadCustomFeedbacks } = req.query as InitChatProps;
let { appId, chatId, loadCustomFeedbacks } = req.query as InitChatProps; if (!appId) {
return jsonRes(res, {
if (!appId) { code: 501,
return jsonRes(res, { message: "You don't have an app yet"
code: 501,
message: "You don't have an app yet"
});
}
// auth app permission
const [{ app, tmbId }, chat] = await Promise.all([
authApp({
req,
authToken: true,
appId,
per: 'r'
}),
chatId ? MongoChat.findOne({ appId, chatId }) : undefined
]);
// auth chat permission
if (chat && !app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
throw new Error(ChatErrEnum.unAuthChat);
}
// get app and history
const [{ history }, { nodes }] = await Promise.all([
getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`
}),
getAppLatestVersion(app._id, app)
]);
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId,
title: chat?.title || '新对话',
userAvatar: undefined,
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(nodes),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro
}
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
}); });
} }
// auth app permission
const [{ app, tmbId }, chat] = await Promise.all([
authApp({
req,
authToken: true,
appId,
per: 'r'
}),
chatId ? MongoChat.findOne({ appId, chatId }) : undefined
]);
// auth chat permission
if (chat && !app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
throw new Error(ChatErrEnum.unAuthChat);
}
// get app and history
const [{ history }, { nodes }] = await Promise.all([
getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`
}),
getAppLatestVersion(app._id, app)
]);
return {
chatId,
appId,
title: chat?.title || '新对话',
userAvatar: undefined,
variables: chat?.variables || {},
history,
app: {
userGuideModule: replaceAppChatConfig({
node: getGuideModule(nodes),
variableList: chat?.variableList,
welcomeText: chat?.welcomeText
}),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro
}
};
} }
export default NextAPI(handler);
export const config = { export const config = {
api: { api: {
responseLimit: '10mb' responseLimit: '10mb'

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d'; import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils'; import { getGuideModule, replaceAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow'; import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getChatItems } from '@fastgpt/service/core/chat/controller'; import { getChatItems } from '@fastgpt/service/core/chat/controller';
@@ -72,7 +72,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
variables: chat?.variables || {}, variables: chat?.variables || {},
history, history,
app: { app: {
userGuideModule: getGuideModule(nodes), userGuideModule: replaceAppChatConfig({
node: getGuideModule(nodes),
variableList: chat?.variableList,
welcomeText: chat?.welcomeText
}),
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,
avatar: app.avatar, avatar: app.avatar,

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response'; import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils'; import { getGuideModule, replaceAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow'; import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import type { InitChatResponse, InitTeamChatProps } from '@/global/core/chat/api.d'; import type { InitChatResponse, InitTeamChatProps } from '@/global/core/chat/api.d';
@@ -73,7 +73,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
variables: chat?.variables || {}, variables: chat?.variables || {},
history, history,
app: { app: {
userGuideModule: getGuideModule(nodes), userGuideModule: replaceAppChatConfig({
node: getGuideModule(nodes),
variableList: chat?.variableList,
welcomeText: chat?.welcomeText
}),
chatModels: getChatModelNameListByModules(nodes), chatModels: getChatModelNameListByModules(nodes),
name: app.name, name: app.name,
avatar: app.avatar, avatar: app.avatar,

View File

@@ -1,6 +1,4 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { jsonRes } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push'; import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { authApp } from '@fastgpt/service/support/permission/auth/app'; import { authApp } from '@fastgpt/service/support/permission/auth/app';
@@ -9,8 +7,12 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team'; import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api'; import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin'; import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise<PostWorkflowDebugResponse> {
const { const {
nodes = [], nodes = [],
edges = [], edges = [],
@@ -18,72 +20,65 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
appId, appId,
pluginId pluginId
} = req.body as PostWorkflowDebugProps; } = req.body as PostWorkflowDebugProps;
try {
await connectToDatabase();
if (!nodes) {
throw new Error('Prams Error');
}
if (!Array.isArray(nodes)) {
throw new Error('Nodes is not array');
}
if (!Array.isArray(edges)) {
throw new Error('Edges is not array');
}
/* user auth */ if (!nodes) {
const [{ teamId, tmbId }] = await Promise.all([ throw new Error('Prams Error');
authCert({
req,
authToken: true
}),
appId && authApp({ req, authToken: true, appId, per: 'r' }),
pluginId && authPluginCrud({ req, authToken: true, pluginId, per: 'r' })
]);
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
/* start process */
const { flowUsages, flowResponses, debugResponse } = await dispatchWorkFlow({
res,
mode: 'debug',
teamId,
tmbId,
user,
appId,
runtimeNodes: nodes,
runtimeEdges: edges,
variables,
query: [],
histories: [],
stream: false,
detail: true,
maxRunTimes: 200
});
pushChatUsage({
appName: '工作流Debug',
appId,
teamId,
tmbId,
source: UsageSourceEnum.fastgpt,
flowUsages
});
jsonRes<PostWorkflowDebugResponse>(res, {
data: {
...debugResponse,
flowResponses
}
});
} catch (err: any) {
jsonRes(res, {
code: 500,
error: err
});
} }
if (!Array.isArray(nodes)) {
throw new Error('Nodes is not array');
}
if (!Array.isArray(edges)) {
throw new Error('Edges is not array');
}
/* user auth */
const [{ teamId, tmbId }] = await Promise.all([
authCert({
req,
authToken: true
}),
appId && authApp({ req, authToken: true, appId, per: 'r' }),
pluginId && authPluginCrud({ req, authToken: true, pluginId, per: 'r' })
]);
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
/* start process */
const { flowUsages, flowResponses, debugResponse } = await dispatchWorkFlow({
res,
mode: 'debug',
teamId,
tmbId,
user,
appId,
runtimeNodes: nodes,
runtimeEdges: edges,
variables,
query: [],
histories: [],
stream: false,
detail: true,
maxRunTimes: 200
});
pushChatUsage({
appName: '工作流Debug',
appId,
teamId,
tmbId,
source: UsageSourceEnum.fastgpt,
flowUsages
});
return {
...debugResponse,
flowResponses
};
} }
export default NextAPI(handler);
export const config = { export const config = {
api: { api: {
bodyParser: { bodyParser: {

View File

@@ -34,11 +34,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
if (!chatId || !chatItemId) { if (!chatId || !chatItemId) {
return res.json({ return res.json({
[NodeOutputKeyEnum.answerText]: `\\n\\n**自动反馈调试**: "${customFeedback}"\\n\\n` [NodeOutputKeyEnum.answerText]: `\\n\\n**自动反馈调试**: "${customFeedback}"\\n\\n`,
text: customFeedback
}); });
} }
return res.json({}); res.json({
text: customFeedback
});
} catch (err) { } catch (err) {
console.log(err); console.log(err);
res.status(500).send(getErrText(err)); res.status(500).send(getErrText(err));

View File

@@ -246,6 +246,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
appId: app._id, appId: app._id,
teamId, teamId,
tmbId: tmbId, tmbId: tmbId,
nodes,
variables: newVariables, variables: newVariables,
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
shareId, shareId,

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react'; import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AppSchema } from '@fastgpt/global/core/app/type.d'; import { AppSchema } from '@fastgpt/global/core/app/type.d';
@@ -25,7 +25,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { formatTime2HM } from '@fastgpt/global/common/string/time'; import { formatTime2HM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context'; import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context';
import { useInterval } from 'ahooks'; import { useInterval, useUpdateEffect } from 'ahooks';
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings')); const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
const PublishHistories = dynamic( const PublishHistories = dynamic(
@@ -341,6 +341,11 @@ const Header = (props: Props) => {
nodes: StoreNodeItemType[]; nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[]; edges: StoreEdgeItemType[];
}>(); }>();
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
useUpdateEffect(() => {
onOpenTest();
}, [workflowTestData]);
return ( return (
<> <>
@@ -351,9 +356,10 @@ const Header = (props: Props) => {
/> />
<ChatTest <ChatTest
ref={ChatTestRef} ref={ChatTestRef}
isOpen={isOpenTest}
{...workflowTestData} {...workflowTestData}
app={app} app={app}
onClose={() => setWorkflowTestData(undefined)} onClose={onCloseTest}
/> />
</> </>
); );

View File

@@ -99,6 +99,7 @@ const EditForm = ({
const selectLLMModel = watch('aiSettings.model'); const selectLLMModel = watch('aiSettings.model');
const datasetSearchSetting = watch('dataset'); const datasetSearchSetting = watch('dataset');
const variables = watch('userGuide.variables'); const variables = watch('userGuide.variables');
const formatVariables = useMemo( const formatVariables = useMemo(
() => formatEditorVariablePickerIcon([...getSystemVariables(t), ...variables]), () => formatEditorVariablePickerIcon([...getSystemVariables(t), ...variables]),
[t, variables] [t, variables]

View File

@@ -12,6 +12,7 @@ import ChatTest from './ChatTest';
import AppCard from './AppCard'; import AppCard from './AppCard';
import EditForm from './EditForm'; import EditForm from './EditForm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type'; import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
const SimpleEdit = ({ appId }: { appId: string }) => { const SimpleEdit = ({ appId }: { appId: string }) => {
const { isPc } = useSystemStore(); const { isPc } = useSystemStore();
@@ -28,6 +29,14 @@ const SimpleEdit = ({ appId }: { appId: string }) => {
// show selected dataset // show selected dataset
useMount(() => { useMount(() => {
loadAllDatasets(); loadAllDatasets();
if (appDetail.version !== 'v2') {
editForm.reset(
appWorkflow2Form({
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes
})
);
}
}); });
return ( return (

View File

@@ -1,8 +1,4 @@
import type { import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d';
AIChatItemType,
ChatItemType,
UserChatItemType
} from '@fastgpt/global/core/chat/type.d';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
@@ -10,12 +6,15 @@ import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { addLog } from '@fastgpt/service/common/system/log'; import { addLog } from '@fastgpt/service/common/system/log';
import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils'; import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
type Props = { type Props = {
chatId: string; chatId: string;
appId: string; appId: string;
teamId: string; teamId: string;
tmbId: string; tmbId: string;
nodes: StoreNodeItemType[];
variables?: Record<string, any>; variables?: Record<string, any>;
isUpdateUseTime: boolean; isUpdateUseTime: boolean;
source: `${ChatSourceEnum}`; source: `${ChatSourceEnum}`;
@@ -30,6 +29,7 @@ export async function saveChat({
appId, appId,
teamId, teamId,
tmbId, tmbId,
nodes,
variables, variables,
isUpdateUseTime, isUpdateUseTime,
source, source,
@@ -72,6 +72,8 @@ export async function saveChat({
chat.variables = variables || {}; chat.variables = variables || {};
await chat.save({ session }); await chat.save({ session });
} else { } else {
const { welcomeText, variableNodes } = splitGuideModule(getGuideModule(nodes));
await MongoChat.create( await MongoChat.create(
[ [
{ {
@@ -79,6 +81,8 @@ export async function saveChat({
teamId, teamId,
tmbId, tmbId,
appId, appId,
variableList: variableNodes,
welcomeText,
variables, variables,
title, title,
source, source,

View File

@@ -288,7 +288,7 @@ export const getWorkflowGlobalVariables = (
t: TFunction t: TFunction
): EditorVariablePickerType[] => { ): EditorVariablePickerType[] => {
const globalVariables = formatEditorVariablePickerIcon( const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes))?.variableModules || [] splitGuideModule(getGuideModule(nodes))?.variableNodes || []
).map((item) => ({ ).map((item) => ({
...item, ...item,
valueType: WorkflowIOValueTypeEnum.any valueType: WorkflowIOValueTypeEnum.any