mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
tmp code
This commit is contained in:
parent
e89a5b6c76
commit
d63311d67a
|
|
@ -33,7 +33,7 @@
|
|||
"scope": "typescriptreact",
|
||||
"prefix": "context",
|
||||
"body": [
|
||||
"import React, { ReactNode } from 'react';",
|
||||
"import React, { type ReactNode } from 'react';",
|
||||
"import { createContext } from 'use-context-selector';",
|
||||
"",
|
||||
"type ContextType = {${1}};",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import type {
|
|||
ChatItemValueItemType,
|
||||
RuntimeUserPromptType,
|
||||
SystemChatItemValueItemType,
|
||||
UserChatItemFileItemType,
|
||||
UserChatItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '../../core/chat/type.d';
|
||||
} from './type';
|
||||
import { ChatFileTypeEnum, ChatRoleEnum } from '../../core/chat/constants';
|
||||
import type {
|
||||
ChatCompletionContentPart,
|
||||
|
|
@ -18,7 +19,7 @@ import type {
|
|||
} from '../../core/ai/type.d';
|
||||
import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants';
|
||||
|
||||
const GPT2Chat = {
|
||||
export const GPT2Chat = {
|
||||
[ChatCompletionRequestMessageRoleEnum.System]: ChatRoleEnum.System,
|
||||
[ChatCompletionRequestMessageRoleEnum.User]: ChatRoleEnum.Human,
|
||||
[ChatCompletionRequestMessageRoleEnum.Assistant]: ChatRoleEnum.AI,
|
||||
|
|
@ -385,9 +386,10 @@ export const chatValue2RuntimePrompt = (value: ChatItemValueItemType[]): Runtime
|
|||
return prompt;
|
||||
};
|
||||
|
||||
export const runtimePrompt2ChatsValue = (
|
||||
prompt: RuntimeUserPromptType
|
||||
): UserChatItemType['value'] => {
|
||||
export const runtimePrompt2ChatsValue = (prompt: {
|
||||
files?: UserChatItemFileItemType[];
|
||||
text?: string;
|
||||
}): UserChatItemType['value'] => {
|
||||
const value: UserChatItemType['value'] = [];
|
||||
if (prompt.files) {
|
||||
prompt.files.forEach((file) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
import { ChatCompletionRequestMessageRoleEnum } from 'core/ai/constants';
|
||||
import type {
|
||||
ChatCompletionContentPart,
|
||||
ChatCompletionFunctionMessageParam,
|
||||
ChatCompletionMessageFunctionCall,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageToolCall,
|
||||
ChatCompletionToolMessageParam
|
||||
} from '../../ai/type';
|
||||
import { ChatFileTypeEnum, ChatRoleEnum } from '../constants';
|
||||
import type { HelperBotChatItemType } from './type';
|
||||
import { GPT2Chat, simpleUserContentPart } from '../adapt';
|
||||
import type {
|
||||
AIChatItemValueItemType,
|
||||
SystemChatItemValueItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '../type';
|
||||
|
||||
export const helperChats2GPTMessages = ({
|
||||
messages,
|
||||
reserveTool = false
|
||||
}: {
|
||||
messages: HelperBotChatItemType[];
|
||||
reserveTool?: boolean;
|
||||
}): ChatCompletionMessageParam[] => {
|
||||
let results: ChatCompletionMessageParam[] = [];
|
||||
|
||||
messages.forEach((item) => {
|
||||
if (item.obj === ChatRoleEnum.System) {
|
||||
const content = item.value?.[0]?.text?.content;
|
||||
if (content) {
|
||||
results.push({
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
content
|
||||
});
|
||||
}
|
||||
} else if (item.obj === ChatRoleEnum.Human) {
|
||||
const value = item.value
|
||||
.map((item) => {
|
||||
if (item.text) {
|
||||
return {
|
||||
type: 'text',
|
||||
text: item.text?.content || ''
|
||||
};
|
||||
}
|
||||
if (item.file) {
|
||||
if (item.file?.type === ChatFileTypeEnum.image) {
|
||||
return {
|
||||
type: 'image_url',
|
||||
key: item.file.key,
|
||||
image_url: {
|
||||
url: item.file.url
|
||||
}
|
||||
};
|
||||
} else if (item.file?.type === ChatFileTypeEnum.file) {
|
||||
return {
|
||||
type: 'file_url',
|
||||
name: item.file?.name || '',
|
||||
url: item.file.url,
|
||||
key: item.file.key
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as ChatCompletionContentPart[];
|
||||
|
||||
results.push({
|
||||
role: ChatCompletionRequestMessageRoleEnum.User,
|
||||
content: simpleUserContentPart(value)
|
||||
});
|
||||
} else {
|
||||
const aiResults: ChatCompletionMessageParam[] = [];
|
||||
|
||||
//AI: 只需要把根节点转化即可
|
||||
item.value.forEach((value, i) => {
|
||||
if ('tool' in value && reserveTool) {
|
||||
const tool_calls: ChatCompletionMessageToolCall[] = [
|
||||
{
|
||||
id: value.tool.id,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: value.tool.functionName,
|
||||
arguments: value.tool.params
|
||||
}
|
||||
}
|
||||
];
|
||||
const toolResponse: ChatCompletionToolMessageParam[] = [
|
||||
{
|
||||
tool_call_id: value.tool.id,
|
||||
role: ChatCompletionRequestMessageRoleEnum.Tool,
|
||||
content: value.tool.response
|
||||
}
|
||||
];
|
||||
aiResults.push({
|
||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
tool_calls
|
||||
});
|
||||
aiResults.push(...toolResponse);
|
||||
} else if ('text' in value && value.text?.content === 'string') {
|
||||
if (!value.text.content && item.value.length > 1) {
|
||||
return;
|
||||
}
|
||||
// Concat text
|
||||
const lastValue = item.value[i - 1];
|
||||
const lastResult = aiResults[aiResults.length - 1];
|
||||
if (lastValue && typeof lastResult?.content === 'string') {
|
||||
lastResult.content += value.text.content;
|
||||
} else {
|
||||
aiResults.push({
|
||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
content: value.text.content
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auto add empty assistant message
|
||||
results = results.concat(
|
||||
aiResults.length > 0
|
||||
? aiResults
|
||||
: [
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
content: ''
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { ObjectIdSchema } from '../../../common/type/mongo';
|
||||
import { z } from 'zod';
|
||||
import { ChatRoleEnum } from '../constants';
|
||||
import {
|
||||
UserChatItemSchema,
|
||||
SystemChatItemSchema,
|
||||
type ChatItemObjItemType,
|
||||
type ChatItemValueItemType,
|
||||
ToolModuleResponseItemSchema
|
||||
} from '../type';
|
||||
|
||||
export enum HelperBotTypeEnum {
|
||||
topAgent = 'topAgent'
|
||||
}
|
||||
export const HelperBotTypeEnumSchema = z.enum(Object.values(HelperBotTypeEnum));
|
||||
export type HelperBotTypeEnumType = z.infer<typeof HelperBotTypeEnumSchema>;
|
||||
|
||||
export const HelperBotChatSchema = z.object({
|
||||
_id: ObjectIdSchema,
|
||||
chatId: z.string(),
|
||||
type: HelperBotTypeEnum,
|
||||
userId: z.string(),
|
||||
createTime: z.date(),
|
||||
updateTime: z.date()
|
||||
});
|
||||
export type HelperBotChatType = z.infer<typeof HelperBotChatSchema>;
|
||||
|
||||
// AI schema
|
||||
const AIChatItemValueItemSchema = z.union([
|
||||
z.object({
|
||||
text: z.object({
|
||||
content: z.string()
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
reasoning: z.object({
|
||||
content: z.string()
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
tool: ToolModuleResponseItemSchema
|
||||
})
|
||||
]);
|
||||
const AIChatItemSchema = z.object({
|
||||
obj: z.literal(ChatRoleEnum.AI),
|
||||
value: z.array(AIChatItemValueItemSchema)
|
||||
});
|
||||
|
||||
const HelperBotChatRoleSchema = z.union([
|
||||
UserChatItemSchema,
|
||||
SystemChatItemSchema,
|
||||
AIChatItemSchema
|
||||
]);
|
||||
export const HelperBotChatItemSchema = z
|
||||
.object({
|
||||
_id: ObjectIdSchema,
|
||||
userId: z.string(),
|
||||
chatId: z.string(),
|
||||
dataId: z.string(),
|
||||
createTime: z.date(),
|
||||
memories: z.record(z.string(), z.any()).nullish()
|
||||
})
|
||||
.and(HelperBotChatRoleSchema);
|
||||
export type HelperBotChatItemType = z.infer<typeof HelperBotChatItemSchema>;
|
||||
|
||||
/* 客户端 UI 展示的类型 */
|
||||
export const HelperBotChatItemSiteSchema = z
|
||||
.object({
|
||||
_id: ObjectIdSchema,
|
||||
dataId: z.string(),
|
||||
createTime: z.date()
|
||||
})
|
||||
.and(HelperBotChatRoleSchema);
|
||||
export type HelperBotChatItemSiteType = z.infer<typeof HelperBotChatItemSiteSchema>;
|
||||
|
||||
/* 具体的 bot 的特有参数 */
|
||||
|
||||
export const topAgentParamsSchema = z.object({
|
||||
role: z.string().nullish(),
|
||||
taskObject: z.string().nullish(),
|
||||
selectedTools: z.array(z.string()).nullish(),
|
||||
selectedDatasets: z.array(z.string()).nullish(),
|
||||
fileUpload: z.boolean().nullish()
|
||||
});
|
||||
export type TopAgentParamsType = z.infer<typeof topAgentParamsSchema>;
|
||||
|
|
@ -1,20 +1,29 @@
|
|||
import { ClassifyQuestionAgentItemType } from '../workflow/template/system/classifyQuestion/type';
|
||||
import type { SearchDataResponseItemType } from '../dataset/type';
|
||||
import type { ChatFileTypeEnum, ChatRoleEnum, ChatSourceEnum, ChatStatusEnum } from './constants';
|
||||
import type { ChatSourceEnum, ChatStatusEnum } from './constants';
|
||||
import { ChatFileTypeEnum, ChatRoleEnum } from './constants';
|
||||
import type { FlowNodeTypeEnum } from '../workflow/node/constant';
|
||||
import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '../workflow/constants';
|
||||
import { NodeOutputKeyEnum } from '../workflow/constants';
|
||||
import type { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants';
|
||||
import type { AppSchema, VariableItemType } from '../app/type';
|
||||
import { AppChatConfigType } from '../app/type';
|
||||
import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d';
|
||||
import { DatasetSearchModeEnum } from '../dataset/constants';
|
||||
import type { DispatchNodeResponseType } from '../workflow/runtime/type.d';
|
||||
import type { DispatchNodeResponseType } from '../workflow/runtime/type';
|
||||
import type { ChatBoxInputType } from '../../../../projects/app/src/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type';
|
||||
import type { FlowNodeInputItemType } from '../workflow/type/io';
|
||||
import type { FlowNodeTemplateType } from '../workflow/type/node.d';
|
||||
import { ChatCompletionMessageParam } from '../ai/type';
|
||||
import type { RequireOnlyOne } from '../../common/type/utils';
|
||||
import z from 'zod';
|
||||
|
||||
/* One tool run response */
|
||||
export type ToolRunResponseItemType = any;
|
||||
/* tool module response */
|
||||
export const ToolModuleResponseItemSchema = z.object({
|
||||
id: z.string(),
|
||||
toolName: z.string(),
|
||||
toolAvatar: z.string(),
|
||||
params: z.string(),
|
||||
response: z.string(),
|
||||
functionName: z.string()
|
||||
});
|
||||
export type ToolModuleResponseItemType = z.infer<typeof ToolModuleResponseItemSchema>;
|
||||
|
||||
/* --------- chat ---------- */
|
||||
export type ChatSchemaType = {
|
||||
|
|
@ -56,33 +65,57 @@ export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
|
|||
};
|
||||
|
||||
/* --------- chat item ---------- */
|
||||
export type UserChatItemFileItemType = {
|
||||
type: `${ChatFileTypeEnum}`;
|
||||
name?: string;
|
||||
key?: string;
|
||||
url: string;
|
||||
};
|
||||
export type UserChatItemValueItemType = {
|
||||
text?: {
|
||||
content: string;
|
||||
};
|
||||
file?: UserChatItemFileItemType;
|
||||
};
|
||||
export type UserChatItemType = {
|
||||
obj: ChatRoleEnum.Human;
|
||||
value: UserChatItemValueItemType[];
|
||||
hideInUI?: boolean;
|
||||
};
|
||||
// User
|
||||
export const UserChatItemFileItemSchema = z.object({
|
||||
type: z.enum(Object.values(ChatFileTypeEnum)),
|
||||
name: z.string().optional(),
|
||||
key: z.string().optional(),
|
||||
url: z.string()
|
||||
});
|
||||
export type UserChatItemFileItemType = z.infer<typeof UserChatItemFileItemSchema>;
|
||||
|
||||
export type SystemChatItemValueItemType = {
|
||||
text?: {
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
export type SystemChatItemType = {
|
||||
obj: ChatRoleEnum.System;
|
||||
value: SystemChatItemValueItemType[];
|
||||
};
|
||||
export const UserChatItemValueItemSchema = z.object({
|
||||
text: z
|
||||
.object({
|
||||
content: z.string()
|
||||
})
|
||||
.optional(),
|
||||
file: UserChatItemFileItemSchema.optional()
|
||||
});
|
||||
export type UserChatItemValueItemType = z.infer<typeof UserChatItemValueItemSchema>;
|
||||
|
||||
export const UserChatItemSchema = z.object({
|
||||
obj: z.literal(ChatRoleEnum.Human),
|
||||
value: z.array(UserChatItemValueItemSchema),
|
||||
hideInUI: z.boolean().optional()
|
||||
});
|
||||
export type UserChatItemType = z.infer<typeof UserChatItemSchema>;
|
||||
|
||||
// System
|
||||
export const SystemChatItemValueItemSchema = z.object({
|
||||
text: z
|
||||
.object({
|
||||
content: z.string()
|
||||
})
|
||||
.nullish()
|
||||
});
|
||||
export type SystemChatItemValueItemType = z.infer<typeof SystemChatItemValueItemSchema>;
|
||||
|
||||
export const SystemChatItemSchema = z.object({
|
||||
obj: z.literal(ChatRoleEnum.System),
|
||||
value: z.array(SystemChatItemValueItemSchema)
|
||||
});
|
||||
export type SystemChatItemType = z.infer<typeof SystemChatItemSchema>;
|
||||
|
||||
// AI
|
||||
export const AdminFbkSchema = z.object({
|
||||
feedbackDataId: z.string(),
|
||||
datasetId: z.string(),
|
||||
collectionId: z.string(),
|
||||
q: z.string(),
|
||||
a: z.string().optional()
|
||||
});
|
||||
export type AdminFbkType = z.infer<typeof AdminFbkSchema>;
|
||||
|
||||
export type AIChatItemValueItemType = {
|
||||
id?: string;
|
||||
|
|
@ -146,14 +179,6 @@ export type ChatItemSchema = ChatItemObjItemType & {
|
|||
time: Date;
|
||||
};
|
||||
|
||||
export type AdminFbkType = {
|
||||
feedbackDataId: string;
|
||||
datasetId: string;
|
||||
collectionId: string;
|
||||
q: string;
|
||||
a?: string;
|
||||
};
|
||||
|
||||
export type ResponseTagItemType = {
|
||||
totalQuoteList?: SearchDataResponseItemType[];
|
||||
llmModuleAccount?: number;
|
||||
|
|
@ -191,8 +216,8 @@ export type ChatItemResponseSchemaType = {
|
|||
|
||||
/* --------- team chat --------- */
|
||||
export type ChatAppListSchema = {
|
||||
apps: AppType[];
|
||||
teamInfo: teamInfoSchema;
|
||||
apps: AppSchema[];
|
||||
teamInfo: any;
|
||||
uid?: string;
|
||||
};
|
||||
|
||||
|
|
@ -217,30 +242,22 @@ export type ChatHistoryItemResType = DispatchNodeResponseType & {
|
|||
};
|
||||
|
||||
/* ---------- node outputs ------------ */
|
||||
export type NodeOutputItemType = {
|
||||
nodeId: string;
|
||||
key: NodeOutputKeyEnum;
|
||||
value: any;
|
||||
};
|
||||
export const NodeOutputItemSchema = z.object({
|
||||
nodeId: z.string(),
|
||||
key: z.enum(Object.values(NodeOutputKeyEnum)),
|
||||
value: z.any()
|
||||
});
|
||||
export type NodeOutputItemType = z.infer<typeof NodeOutputItemSchema>;
|
||||
|
||||
/* One tool run response */
|
||||
export type ToolRunResponseItemType = any;
|
||||
/* tool module response */
|
||||
export type ToolModuleResponseItemType = {
|
||||
id: string;
|
||||
toolName: string; // tool name
|
||||
toolAvatar: string;
|
||||
params: string; // tool params
|
||||
response: string;
|
||||
functionName: string;
|
||||
};
|
||||
export const ToolCiteLinksSchema = z.object({
|
||||
name: z.string(),
|
||||
url: z.string()
|
||||
});
|
||||
export type ToolCiteLinksType = z.infer<typeof ToolCiteLinksSchema>;
|
||||
|
||||
export type ToolCiteLinksType = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
/* dispatch run time */
|
||||
export type RuntimeUserPromptType = {
|
||||
files: UserChatItemValueItemType['file'][];
|
||||
text: string;
|
||||
};
|
||||
export const RuntimeUserPromptSchema = z.object({
|
||||
files: z.array(UserChatItemFileItemSchema),
|
||||
text: z.string()
|
||||
});
|
||||
export type RuntimeUserPromptType = z.infer<typeof RuntimeUserPromptSchema>;
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type ChatHistoryItemResType,
|
||||
type ChatItemType,
|
||||
type UserChatItemValueItemType
|
||||
} from './type.d';
|
||||
} from './type';
|
||||
import { sliceStrStartEnd } from '../../common/string/tools';
|
||||
import { PublishChannelEnum } from '../../support/outLink/constant';
|
||||
import { removeDatasetCiteText } from '../ai/llm/utils';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { PaginationPropsSchema, PaginationResponseSchema } from '../../../type';
|
||||
import {
|
||||
type HelperBotChatItemSiteType,
|
||||
HelperBotTypeEnumSchema,
|
||||
topAgentParamsSchema
|
||||
} from '../../../../core/chat/helperBot/type';
|
||||
import { z } from 'zod';
|
||||
import type { PaginationResponse } from '../../../../../web/common/fetch/type';
|
||||
|
||||
// 分页获取记录
|
||||
export const GetHelperBotChatRecordsParamsSchema = z
|
||||
.object({
|
||||
type: HelperBotTypeEnumSchema,
|
||||
chatId: z.string()
|
||||
})
|
||||
.and(PaginationPropsSchema);
|
||||
export type GetHelperBotChatRecordsParamsType = z.infer<typeof GetHelperBotChatRecordsParamsSchema>;
|
||||
export type GetHelperBotChatRecordsResponseType = PaginationResponse<HelperBotChatItemSiteType>;
|
||||
|
||||
// 删除单组对话
|
||||
export const DeleteHelperBotChatParamsSchema = z.object({
|
||||
type: HelperBotTypeEnumSchema,
|
||||
chatId: z.string(),
|
||||
chatItemId: z.string()
|
||||
});
|
||||
export type DeleteHelperBotChatParamsType = z.infer<typeof DeleteHelperBotChatParamsSchema>;
|
||||
|
||||
// 获取文件上传签名
|
||||
export const GetHelperBotFilePresignParamsSchema = z.object({
|
||||
type: HelperBotTypeEnumSchema,
|
||||
chatId: z.string(),
|
||||
filename: z.string()
|
||||
});
|
||||
export type GetHelperBotFilePresignParamsType = z.infer<typeof GetHelperBotFilePresignParamsSchema>;
|
||||
|
||||
// 获取文件预览链接
|
||||
export const GetHelperBotFilePreviewParamsSchema = z.object({
|
||||
key: z.string().min(1)
|
||||
});
|
||||
export type GetHelperBotFilePreviewParamsType = z.infer<typeof GetHelperBotFilePreviewParamsSchema>;
|
||||
export const GetHelperBotFilePreviewResponseSchema = z.string();
|
||||
|
||||
export const HelperBotCompletionsParamsSchema = z.object({
|
||||
chatId: z.string(),
|
||||
chatItemId: z.string(),
|
||||
query: z.string(),
|
||||
files: z.array(
|
||||
z.object({
|
||||
type: z.enum(['image', 'file']),
|
||||
key: z.string(),
|
||||
url: z.string().optional(),
|
||||
name: z.string()
|
||||
})
|
||||
),
|
||||
metadata: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('topAgent'),
|
||||
data: topAgentParamsSchema
|
||||
})
|
||||
])
|
||||
});
|
||||
export type HelperBotCompletionsParamsType = z.infer<typeof HelperBotCompletionsParamsSchema>;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { z } from 'zod';
|
||||
import type { OpenAPIPath } from '../../../type';
|
||||
import {
|
||||
DeleteHelperBotChatParamsSchema,
|
||||
GetHelperBotChatRecordsParamsSchema,
|
||||
HelperBotCompletionsParamsSchema
|
||||
} from './api';
|
||||
import { TagsMap } from '../../../tag';
|
||||
|
||||
export const HelperBotPath: OpenAPIPath = {
|
||||
'/core/chat/helperBot/getRecords': {
|
||||
get: {
|
||||
summary: '分页获取记录',
|
||||
description: '分页获取记录',
|
||||
tags: [TagsMap.helperBot],
|
||||
requestParams: {
|
||||
query: GetHelperBotChatRecordsParamsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功返回记录列表',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.array(z.any())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/helperBot/deleteRecord': {
|
||||
delete: {
|
||||
summary: '删除单组对话',
|
||||
description: '删除单组对话',
|
||||
tags: [TagsMap.helperBot],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: DeleteHelperBotChatParamsSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功删除记录',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.any()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/helperBot/completions': {
|
||||
post: {
|
||||
summary: '辅助助手对话接口',
|
||||
description: '辅助助手对话接口',
|
||||
tags: [TagsMap.helperBot],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: HelperBotCompletionsParamsSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功返回处理结果',
|
||||
content: {
|
||||
'application/stream+json': {
|
||||
schema: z.any()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -12,12 +12,14 @@ import {
|
|||
StopV2ChatResponseSchema
|
||||
} from './api';
|
||||
import { TagsMap } from '../../tag';
|
||||
import { HelperBotPath } from './helperBot';
|
||||
|
||||
export const ChatPath: OpenAPIPath = {
|
||||
...ChatSettingPath,
|
||||
...ChatFavouriteAppPath,
|
||||
...ChatFeedbackPath,
|
||||
...ChatHistoryPath,
|
||||
...HelperBotPath,
|
||||
|
||||
'/v2/chat/stop': {
|
||||
post: {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ export const openAPIDocument = createDocument({
|
|||
{
|
||||
name: '管理员-插件管理',
|
||||
tags: [TagsMap.pluginAdmin, TagsMap.pluginMarketplace, TagsMap.pluginToolAdmin]
|
||||
},
|
||||
{
|
||||
name: '系统接口',
|
||||
tags: [TagsMap.helperBot]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export const TagsMap = {
|
||||
/* Core */
|
||||
helperBot: '辅助助手',
|
||||
// Agent - log
|
||||
appLog: 'Agent 日志',
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ export const TagsMap = {
|
|||
pluginMarketplace: '插件市场',
|
||||
pluginAdmin: '管理员插件管理',
|
||||
pluginToolAdmin: '管理员系统工具管理',
|
||||
// Data
|
||||
|
||||
// Admin
|
||||
adminDashboard: '管理员仪表盘'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,3 +27,26 @@ export const formatSuccessResponse = <T>(data: T) => {
|
|||
data
|
||||
});
|
||||
};
|
||||
|
||||
export const PaginationPropsSchema = z
|
||||
.object({
|
||||
pageSize: z.union([z.number(), z.string()]),
|
||||
// offset 和 pageNum 只能传其一
|
||||
offset: z.union([z.number(), z.string()]).optional(),
|
||||
pageNum: z.union([z.number(), z.string()]).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => (typeof data.offset !== 'undefined') !== (typeof data.pageNum !== 'undefined'),
|
||||
{ message: 'offset 和 pageNum 必须且只能传一个' }
|
||||
);
|
||||
export type PaginationPropsType = z.infer<typeof PaginationPropsSchema>;
|
||||
|
||||
export const PaginationResponseSchema = <T extends z.ZodType>(item: T) =>
|
||||
z.object({
|
||||
total: z.number(),
|
||||
list: z.array(item)
|
||||
});
|
||||
export type PaginationResponseType<T = any> = {
|
||||
total: number;
|
||||
list: T[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { HistoryItemType } from '../../core/chat/type.d';
|
||||
import type { HistoryItemType } from '../../core/chat/type';
|
||||
import type { OutLinkSchema } from './type.d';
|
||||
|
||||
export type AuthOutLinkInitProps = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||
import { S3PrivateBucket } from '../../buckets/private';
|
||||
import { S3Sources } from '../../type';
|
||||
import {
|
||||
type CheckHelperBotFileKeys,
|
||||
type DelChatFileByPrefixParams,
|
||||
DelChatFileByPrefixSchema,
|
||||
HelperBotFileUploadSchema
|
||||
} from './type';
|
||||
import { differenceInHours } from 'date-fns';
|
||||
import { S3Buckets } from '../../constants';
|
||||
import path from 'path';
|
||||
import { getFileS3Key } from '../../utils';
|
||||
|
||||
export class S3HelperBotSource {
|
||||
private bucket: S3PrivateBucket;
|
||||
private static instance: S3HelperBotSource;
|
||||
|
||||
constructor() {
|
||||
this.bucket = new S3PrivateBucket();
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
return (this.instance ??= new S3HelperBotSource());
|
||||
}
|
||||
|
||||
static parseFileUrl(url: string | URL) {
|
||||
try {
|
||||
const parseUrl = new URL(url);
|
||||
const pathname = decodeURIComponent(parseUrl.pathname);
|
||||
// 非 S3 key
|
||||
if (!pathname.startsWith(`/${S3Buckets.private}/${S3Sources.helperBot}/`)) {
|
||||
return {
|
||||
filename: '',
|
||||
extension: '',
|
||||
imageParsePrefix: ''
|
||||
};
|
||||
}
|
||||
|
||||
const filename = pathname.split('/').pop() || 'file';
|
||||
const extension = path.extname(filename);
|
||||
|
||||
return {
|
||||
filename,
|
||||
extension: extension.replace('.', ''),
|
||||
imageParsePrefix: `${pathname.replace(`/${S3Buckets.private}/`, '').replace(extension, '')}-parsed`
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
filename: '',
|
||||
extension: '',
|
||||
imageParsePrefix: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
parseKey(key: string) {
|
||||
const [type, chatId, userId, filename] = key.split('/');
|
||||
return { type, chatId, userId, filename };
|
||||
}
|
||||
|
||||
// 获取文件流
|
||||
getFileStream(key: string) {
|
||||
return this.bucket.getObject(key);
|
||||
}
|
||||
|
||||
// 获取文件状态
|
||||
getFileStat(key: string) {
|
||||
return this.bucket.statObject(key);
|
||||
}
|
||||
|
||||
// 获取文件元数据
|
||||
async getFileMetadata(key: string) {
|
||||
const stat = await this.getFileStat(key);
|
||||
if (!stat) return { filename: '', extension: '', contentLength: 0, contentType: '' };
|
||||
|
||||
const contentLength = stat.size;
|
||||
const filename: string = decodeURIComponent(stat.metaData['origin-filename']);
|
||||
const extension = parseFileExtensionFromUrl(filename);
|
||||
const contentType: string = stat.metaData['content-type'];
|
||||
return {
|
||||
filename,
|
||||
extension,
|
||||
contentType,
|
||||
contentLength
|
||||
};
|
||||
}
|
||||
|
||||
async createGetFileURL(params: { key: string; expiredHours?: number; external: boolean }) {
|
||||
const { key, expiredHours = 1, external = false } = params; // 默认一个小时
|
||||
|
||||
if (external) {
|
||||
return await this.bucket.createExternalUrl({ key, expiredHours });
|
||||
}
|
||||
return await this.bucket.createPreviewUrl({ key, expiredHours });
|
||||
}
|
||||
|
||||
async createUploadFileURL(params: CheckHelperBotFileKeys) {
|
||||
const { type, chatId, userId, filename, expiredTime } = HelperBotFileUploadSchema.parse(params);
|
||||
const { fileKey } = getFileS3Key.helperBot({ type, chatId, userId, filename });
|
||||
return await this.bucket.createPostPresignedUrl(
|
||||
{ rawKey: fileKey, filename },
|
||||
{ expiredHours: expiredTime ? differenceInHours(new Date(), expiredTime) : 24 }
|
||||
);
|
||||
}
|
||||
|
||||
deleteFilesByPrefix(params: DelChatFileByPrefixParams) {
|
||||
const { type, chatId, userId } = DelChatFileByPrefixSchema.parse(params);
|
||||
|
||||
const prefix = [S3Sources.helperBot, type, userId, chatId].filter(Boolean).join('/');
|
||||
return this.bucket.addDeleteJob({ prefix });
|
||||
}
|
||||
|
||||
deleteFileByKey(key: string) {
|
||||
return this.bucket.addDeleteJob({ key });
|
||||
}
|
||||
}
|
||||
|
||||
export function getS3HelperBotSource() {
|
||||
return S3HelperBotSource.getInstance();
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { z } from 'zod';
|
||||
import { HelperBotTypeEnumSchema } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
|
||||
export const HelperBotFileUploadSchema = z.object({
|
||||
type: HelperBotTypeEnumSchema,
|
||||
chatId: z.string().nonempty(),
|
||||
userId: z.string().nonempty(),
|
||||
filename: z.string().nonempty(),
|
||||
expiredTime: z.date().optional()
|
||||
});
|
||||
export type CheckHelperBotFileKeys = z.infer<typeof HelperBotFileUploadSchema>;
|
||||
|
||||
export const DelChatFileByPrefixSchema = z.object({
|
||||
type: HelperBotTypeEnumSchema,
|
||||
chatId: z.string().nonempty().optional(),
|
||||
userId: z.string().nonempty().optional()
|
||||
});
|
||||
export type DelChatFileByPrefixParams = z.infer<typeof DelChatFileByPrefixSchema>;
|
||||
|
|
@ -17,7 +17,14 @@ export type ExtensionType = keyof typeof Mimes;
|
|||
|
||||
export type S3OptionsType = typeof defaultS3Options;
|
||||
|
||||
export const S3SourcesSchema = z.enum(['avatar', 'chat', 'dataset', 'temp', 'rawText']);
|
||||
export const S3SourcesSchema = z.enum([
|
||||
'avatar',
|
||||
'chat',
|
||||
'dataset',
|
||||
'temp',
|
||||
'rawText',
|
||||
'helperBot'
|
||||
]);
|
||||
export const S3Sources = S3SourcesSchema.enum;
|
||||
export type S3SourceType = z.infer<typeof S3SourcesSchema>;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
|
|||
import path from 'node:path';
|
||||
import type { ParsedFileContentS3KeyParams } from './sources/dataset/type';
|
||||
import { EndpointUrl } from '@fastgpt/global/common/file/constants';
|
||||
import type { HelperBotTypeEnumType } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
|
||||
// S3文件名最大长度配置
|
||||
export const S3_FILENAME_MAX_LENGTH = 50;
|
||||
|
|
@ -205,6 +206,25 @@ export const getFileS3Key = {
|
|||
};
|
||||
},
|
||||
|
||||
helperBot: ({
|
||||
type,
|
||||
chatId,
|
||||
userId,
|
||||
filename
|
||||
}: {
|
||||
type: HelperBotTypeEnumType;
|
||||
chatId: string;
|
||||
userId: string;
|
||||
filename: string;
|
||||
}) => {
|
||||
const { formatedFilename, extension } = getFormatedFilename(filename);
|
||||
const basePrefix = [S3Sources.helperBot, type, userId, chatId].filter(Boolean).join('/');
|
||||
return {
|
||||
fileKey: [basePrefix, `${formatedFilename}${extension ? `.${extension}` : ''}`].join('/'),
|
||||
fileParsedPrefix: [basePrefix, `${formatedFilename}-parsed`].join('/')
|
||||
};
|
||||
},
|
||||
|
||||
// 上传数据集的文件的解析结果的图片的 Key
|
||||
dataset: (params: ParsedFileContentS3KeyParams) => {
|
||||
const { datasetId, filename } = params;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { connectionMongo, getMongoModel } from '../../../common/mongo';
|
||||
const { Schema } = connectionMongo;
|
||||
import { helperBotChatItemCollectionName } from './constants';
|
||||
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import type { HelperBotChatItemType } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
|
||||
const HelperBotChatItemSchema = new Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
chatId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
dataId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
obj: {
|
||||
type: String,
|
||||
require: true,
|
||||
enum: Object.values(ChatRoleEnum)
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
require: true
|
||||
},
|
||||
memories: Object
|
||||
});
|
||||
|
||||
HelperBotChatItemSchema.index({ userId: 1, chatId: 1, dataId: 1, obj: 1 }, { unique: true });
|
||||
|
||||
export const MongoHelperBotChatItem = getMongoModel<HelperBotChatItemType>(
|
||||
helperBotChatItemCollectionName,
|
||||
HelperBotChatItemSchema
|
||||
);
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { connectionMongo, getMongoModel } from '../../../common/mongo';
|
||||
const { Schema } = connectionMongo;
|
||||
import { helperBotChatCollectionName } from './constants';
|
||||
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import type { HelperBotChatType } from '../../../../global/core/chat/helperBot/type';
|
||||
|
||||
const HelperBotChatSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: Object.values(HelperBotTypeEnum)
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
chatId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
updateTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
}
|
||||
});
|
||||
|
||||
HelperBotChatSchema.index({ type: 1, userId: 1, chatId: 1 }, { unique: true });
|
||||
|
||||
export const MongoHelperBotChat = getMongoModel<HelperBotChatType>(
|
||||
helperBotChatCollectionName,
|
||||
HelperBotChatSchema
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export const helperBotChatCollectionName = 'helper_bot_chats';
|
||||
export const helperBotChatItemCollectionName = 'helper_bot_chat_items';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import { dispatchTopAgent } from './topAgent';
|
||||
|
||||
export const dispatchMap = {
|
||||
[HelperBotTypeEnum.topAgent]: dispatchTopAgent
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type { HelperBotDispatchParamsType } from '../type';
|
||||
import { helperChats2GPTMessages } from '@fastgpt/global/core/chat/helperBot/adaptor';
|
||||
|
||||
export const dispatchTopAgent = async (props: HelperBotDispatchParamsType) => {
|
||||
const { query, files, metadata, histories } = props;
|
||||
const messages = helperChats2GPTMessages({
|
||||
messages: histories,
|
||||
reserveTool: false
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
export const getPrompt = ({ resourceList }: { resourceList: string }) => {
|
||||
return `<!-- 流程搭建模板设计系统 -->
|
||||
|
||||
<role>
|
||||
你是一个专业的**流程架构师**和**智能化搭建专家**,专门帮助搭建者设计可复用的Agent执行流程模板。
|
||||
|
||||
**核心价值**:让搭建者能够快速创建高质量的执行流程,为后续用户提供标准化的问题解决方案。
|
||||
|
||||
**核心能力**:
|
||||
- 流程抽象化:将具体需求抽象为通用流程模板
|
||||
- 参数化设计:识别可变参数和固定逻辑
|
||||
- 能力边界识别:严格基于系统现有工具、知识库、文件处理等能力进行规划
|
||||
- 复用性优化:确保模板在不同场景下的适应性
|
||||
</role>
|
||||
|
||||
<mission>
|
||||
**核心目标**:为搭建者设计可复用的执行流程,包含:
|
||||
1. 明确的步骤序列
|
||||
2. 标准化的工具调用
|
||||
3. 合理的决策点设计
|
||||
4. 100%基于系统能力的可行性保证
|
||||
|
||||
**输出价值**:
|
||||
- 搭建者可以直接使用或参考这个流程设计
|
||||
- 最终用户可以通过这个流程解决相关问题
|
||||
- 系统可以保证完全的可执行性
|
||||
</mission>
|
||||
|
||||
<info_collection_phase>
|
||||
当处于信息收集阶段时:
|
||||
|
||||
**重要前提**:你需要为搭建者设计可复用的执行流程模板,而不仅仅是解决单个问题。
|
||||
|
||||
**信息收集目标**:收集设计高质量流程模板所必需的核心信息,包括:
|
||||
|
||||
1. **任务类型与场景识别**(首要任务)
|
||||
- 通过开放式提问,了解用户想要实现的具体功能
|
||||
- 基于用户描述,识别和归纳出任务所属的场景类型
|
||||
- 理解任务的核心特征、目标和定位
|
||||
- 为后续信息收集确定方向和重点
|
||||
|
||||
2. **能力边界确认**(最关键,必须优先确认)
|
||||
- **基于实际工具列表**确认系统能力:
|
||||
* 需要使用哪些核心工具(从可用工具列表中选择)
|
||||
* 这些工具的具体功能和限制条件
|
||||
* 工具之间的组合使用方式
|
||||
- **明确不支持的功能范围**(重点):
|
||||
* 哪些功能系统当前无法实现
|
||||
* 哪些操作没有对应的工具支持
|
||||
* 用户可能期望但实际不可行的需求
|
||||
- **技术限制和约束条件**:
|
||||
* 数据格式、大小、性能等限制
|
||||
* 第三方服务的可用性和配置需求
|
||||
* 用户权限和资源约束
|
||||
|
||||
3. **流程定位信息**
|
||||
- 目标用户群体和典型使用场景
|
||||
- 解决问题的具体类型和适用范围
|
||||
- 流程的核心价值和预期效果
|
||||
|
||||
4. **输入输出规范**
|
||||
- 流程的输入参数类型、格式和来源
|
||||
- 输出结果的规范、格式和目标
|
||||
- 参数约束条件(必选/可选/默认值/取值范围)
|
||||
|
||||
5. **可变逻辑识别**
|
||||
- 哪些步骤需要根据参数动态调整
|
||||
- 决策点的判断条件和分支逻辑
|
||||
- 可配置的工具选项和参数映射关系
|
||||
|
||||
**关键原则**:
|
||||
- **能力边界优先**:必须先确认系统能做什么,再设计流程细节
|
||||
- **工具列表约束**:严格基于可用工具列表,不假设任何未提供的能力
|
||||
- **场景分类明确**:首先明确任务类型,指导后续信息收集方向
|
||||
- **问题精准聚焦**:每个问题都直接服务于输出准确的信息和工具列表
|
||||
- **明确不可行项**:重点确认哪些功能不能做,避免后续生成无法执行的流程
|
||||
|
||||
**信息收集顺序建议**:
|
||||
1. 先问任务类型/场景(确定大方向,对应输出type字段)
|
||||
2. 再问能力边界(确认可行性,识别可用工具)
|
||||
3. 然后问流程定位(明确具体目标)
|
||||
4. 最后问输入输出、可变逻辑(完善设计细节)
|
||||
|
||||
|
||||
**输出格式**:
|
||||
|
||||
**重要:信息收集阶段的所有回复必须使用JSON格式,包含推理过程**
|
||||
|
||||
直接输出以下格式的JSON(不要添加代码块标记):
|
||||
{
|
||||
"reasoning": "为什么问这个问题的推理过程:基于什么考虑、希望收集什么信息、对后续有什么帮助",
|
||||
"question": "实际向用户提出的问题内容"
|
||||
}
|
||||
|
||||
问题内容可以是开放式问题,也可以包含选项:
|
||||
|
||||
开放式问题示例:
|
||||
{
|
||||
"reasoning": "需要首先了解任务的基本定位和目标场景,这将决定后续需要确认的工具类型和能力边界",
|
||||
"question": "我想了解一下您希望这个流程模板实现什么功能?能否详细描述一下具体要处理什么样的任务或问题?"
|
||||
}
|
||||
|
||||
选择题示例:
|
||||
{
|
||||
"reasoning": "需要确认参数化设计的重点方向,这将影响流程模板的灵活性设计",
|
||||
"question": "关于流程的参数化设计,用户最需要调整的是:\\nA. 输入数据源(不同类型的数据库/文件)\\nB. 处理参数(阈值、过滤条件、算法选择)\\nC. 输出格式(报告类型、文件格式、目标系统)\\nD. 执行环境(触发方式、频率、并发度)\\n\\n请选择最符合的选项,或输入您的详细回答:"
|
||||
}
|
||||
|
||||
选项设计原则:
|
||||
1. 选项要覆盖主要可能性(3-4个为佳)
|
||||
2. 包含"其他"选项让用户可以自由回答
|
||||
3. 选项要简洁明了,便于快速理解
|
||||
4. 当问题涉及量化、分类、优先级时优先使用选择题
|
||||
|
||||
适合选择题的场景:
|
||||
- 经验水平判断(初学者/有经验/熟练/专家)
|
||||
- 优先级排序(时间/质量/成本/创新)
|
||||
- 任务类型分类(分析/设计/开发/测试)
|
||||
- 满意度评估(非常满意/满意/一般/不满意)
|
||||
- 复杂度判断(简单/中等/复杂/极复杂)
|
||||
|
||||
避免的行为:
|
||||
- 不要为所有问题都强制提供选项
|
||||
- 选项之间要有明显的区分度
|
||||
- 不要使用过于技术化的术语
|
||||
</info_collection_phase>
|
||||
|
||||
<capability_boundary_enforcement>
|
||||
**系统能力边界确认**:
|
||||
|
||||
**动态约束原则**:
|
||||
1. **只规划现有能力**:只能使用系统当前提供的工具和功能
|
||||
2. **基于实际能力判断**:如果系统有编程工具,就可以规划编程任务
|
||||
3. **能力适配规划**:根据可用工具库的能力边界来设计流程
|
||||
4. **避免能力假设**:不能假设系统有未明确提供的能力
|
||||
|
||||
**规划前自我检查**:
|
||||
- 这个步骤需要什么具体能力?
|
||||
- 当前系统中是否有对应的工具提供这种能力?
|
||||
- 用户是否具备使用该工具的条件?
|
||||
- 如果没有合适的工具,能否用现有能力组合实现?
|
||||
|
||||
**能力发现机制**:
|
||||
- 优先使用系统中明确提供的工具
|
||||
- 探索现有工具的组合能力
|
||||
- 基于实际可用能力设计解决方案
|
||||
- 避免依赖系统中不存在的能力
|
||||
|
||||
**重要提醒**:请基于下面提供的可用工具列表,仔细分析系统能力边界,确保规划的每个步骤都有对应的工具支持。
|
||||
</capability_boundary_enforcement>
|
||||
|
||||
<plan_generation_phase>
|
||||
当处于计划生成阶段时:
|
||||
|
||||
<resource_definitions>
|
||||
**系统资源定义**(重要:理解三类资源的本质区别)
|
||||
|
||||
**工具 (Tools)**:
|
||||
- 定义:可以执行特定功能的能力模块
|
||||
- 功能:执行操作、调用API、处理数据、生成内容等
|
||||
- 特点:主动执行,产生结果或副作用
|
||||
- 示例:搜索引擎、数据库操作、邮件发送、内容生成
|
||||
|
||||
**知识库 (Knowledges)**:
|
||||
- 定义:系统上已经搭建好的文件存储系统,包含特定领域的结构化信息
|
||||
- 功能:存储和检索信息,提供领域知识查询
|
||||
- 特点:被动查询,返回已存储的信息
|
||||
- 示例:产品文档库、技术手册、行业知识库
|
||||
|
||||
**系统功能 (System Features)**:
|
||||
- 定义:平台级的功能开关,控制执行流程的特殊能力
|
||||
- 功能:影响任务执行方式的系统级配置
|
||||
- 特点:开关控制,改变交互模式
|
||||
- 示例:文件上传、用户交互、实时数据流
|
||||
|
||||
**关键区别**:
|
||||
- 工具 = "做事情"(执行动作、调用服务、处理数据)
|
||||
- 知识库 = "查信息"(检索已有知识、获取领域信息)
|
||||
- 系统功能 = "改变模式"(启用特殊交互方式、系统级能力)
|
||||
|
||||
**选择建议**:
|
||||
- 需要执行操作(搜索、发送、计算、转换)→ 选择工具
|
||||
- 需要查询特定领域的信息(产品资料、技术文档、行业知识)→ 选择知识库
|
||||
- 需要用户提供文件/特殊交互方式 → 启用系统功能
|
||||
- 三者可以配合使用:例如用搜索工具获取实时信息,用知识库补充领域知识,启用文件上传让用户提供私有数据
|
||||
</resource_definitions>
|
||||
|
||||
**可用资源列表**:
|
||||
"""
|
||||
${resourceList}
|
||||
"""
|
||||
|
||||
**计划生成要求**:
|
||||
1. 严格按照JSON格式输出
|
||||
2. **严格确保所有引用的资源都在可用资源列表中** - 这是硬性要求
|
||||
3. 考虑搭建者的实际约束条件(时间、资源、技能等)
|
||||
4. **绝不要使用任何不在可用资源列表中的资源** - 违背此项将导致计划被拒绝
|
||||
|
||||
**🚨 资源使用严格限制(极其重要)**:
|
||||
|
||||
**资源识别规则**:
|
||||
1. 在上面的"## 可用资源列表"中查找所有可用资源
|
||||
2. 每个资源ID后面都有标签:[工具] 或 [知识库]
|
||||
3. 输出时必须根据标签确定 type 值:
|
||||
- 标签是 [工具] → "type": "tool"
|
||||
- 标签是 [知识库] → "type": "knowledge"
|
||||
|
||||
**输出格式要求**:
|
||||
- ✅ 必须使用对象数组格式:[{"id": "...", "type": "..."}]
|
||||
- ✅ 资源ID必须完全匹配列表中的ID(包括大小写、特殊字符)
|
||||
- ❌ 不要使用字符串数组格式:["...", "..."]
|
||||
- ❌ 不要猜测 type 值,必须根据列表中的标签确定
|
||||
|
||||
**输出前的自我检查步骤**:
|
||||
1. 查看你选择的每个资源ID,它在列表中的标签是什么?
|
||||
2. 如果标签是 [工具] → 设置 "type": "tool"
|
||||
3. 如果标签是 [知识库] → 设置 "type": "knowledge"
|
||||
4. 确保每个资源都有 id 和 type 两个字段
|
||||
|
||||
**常见错误避免**:
|
||||
- ❌ 不要凭空想象资源名称
|
||||
- ❌ 不要使用通用描述如"数据库工具"而不指定具体ID
|
||||
- ❌ 不要引用"可能"存在但未在列表中明确的资源
|
||||
- ❌ 不要输出字符串数组,必须是对象数组
|
||||
- ❌ 不要把 [知识库] 标签的资源设置为 type: "tool"
|
||||
- ✅ 必须根据列表中的标签准确设置 type 值
|
||||
- ✅ 基于实际可用的资源进行规划
|
||||
|
||||
**深度分析框架**(内部思考过程,不输出):
|
||||
🔍 第一层:任务本质分析
|
||||
- 识别用户的核心目标和真实意图
|
||||
- 分析任务的复杂度、范围和关键约束
|
||||
- 确定主要的功能需求和预期成果
|
||||
|
||||
📋 第二层:资源需求识别
|
||||
根据任务特点,识别需要的三类资源:
|
||||
- 需要哪些工具来执行操作?(搜索、计算、生成、发送等)
|
||||
- 需要哪些知识库来获取领域知识?(产品资料、技术文档等)
|
||||
- 需要哪些系统功能来改变交互模式?(是否需要用户上传文件?)
|
||||
|
||||
🎯 第三层:精确资源匹配
|
||||
从可用资源列表中选择最合适的资源:
|
||||
- 工具选择:基于任务细节选择功能最匹配的工具
|
||||
- 知识库选择:基于领域需求选择相关知识库
|
||||
- 系统功能判断:
|
||||
* 是否需要用户的私有文件?→ 启用 file_upload
|
||||
* 数据能否通过工具获取?→ 不需要 file_upload
|
||||
|
||||
🔧 第四层:资源整合
|
||||
- 收集所有需要的工具、知识库和系统功能
|
||||
- 去除重复项
|
||||
- 确保所有工具和知识库ID都在可用列表中
|
||||
- 形成完整的 resources 配置
|
||||
|
||||
**输出要求**:
|
||||
**重要:只输出JSON,不要添加任何解释文字、代码块标记或其他内容!**
|
||||
|
||||
直接输出以下格式的JSON:
|
||||
{
|
||||
"task_analysis": {
|
||||
"goal": "任务的核心目标描述",
|
||||
"role": "该流程的角色信息",
|
||||
"key_features": "收集到的信息,对任务的深度理解和定位"
|
||||
},
|
||||
"reasoning": "详细说明所有资源的选择理由:工具、知识库和系统功能如何协同工作来完成任务目标",
|
||||
"resources": {
|
||||
"tools": [
|
||||
{"id": "工具ID", "type": "tool"}
|
||||
],
|
||||
"knowledges": [
|
||||
{"id": "知识库ID", "type": "knowledge"}
|
||||
],
|
||||
"system_features": {
|
||||
"file_upload": {
|
||||
"enabled": true/false,
|
||||
"purpose": "说明原因(enabled=true时必填)",
|
||||
"file_types": ["可选的文件类型"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
**字段说明**:
|
||||
- task_analysis: 提供对任务的深度理解和角色定义
|
||||
- reasoning: 说明所有资源(工具+知识库+系统功能)的选择理由和协同关系
|
||||
- resources: 资源配置对象,包含三类资源
|
||||
* tools: 工具数组,每个对象包含 id 和 type(值为"tool")
|
||||
* knowledges: 知识库数组,每个对象包含 id 和 type(值为"knowledge")
|
||||
* system_features: 系统功能配置对象
|
||||
- file_upload.enabled: 是否需要文件上传(必填)
|
||||
- file_upload.purpose: 为什么需要(enabled=true时必填)
|
||||
- file_upload.file_types: 建议的文件类型(可选),如["pdf", "xlsx"]
|
||||
|
||||
**✅ 正确示例1**(需要文件上传):
|
||||
{
|
||||
"task_analysis": {
|
||||
"goal": "分析用户的财务报表数据",
|
||||
"role": "财务数据分析专家"
|
||||
},
|
||||
"reasoning": "使用数据分析工具处理Excel数据,需要用户上传自己的财务报表文件",
|
||||
"resources": {
|
||||
"tools": [
|
||||
{"id": "data_analysis/tool", "type": "tool"}
|
||||
],
|
||||
"knowledges": [],
|
||||
"system_features": {
|
||||
"file_upload": {
|
||||
"enabled": true,
|
||||
"purpose": "需要您上传财务报表文件(Excel或PDF格式)进行数据提取和分析",
|
||||
"file_types": ["xlsx", "xls", "pdf"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
**✅ 正确示例2**(不需要文件上传):
|
||||
{
|
||||
"reasoning": "使用搜索工具获取实时信息,结合知识库的专业知识",
|
||||
"resources": {
|
||||
"tools": [
|
||||
{"id": "metaso/metasoSearch", "type": "tool"}
|
||||
],
|
||||
"knowledges": [
|
||||
{"id": "travel_kb", "type": "knowledge"}
|
||||
],
|
||||
"system_features": {
|
||||
"file_upload": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
**❌ 错误示例1**(使用旧格式):
|
||||
{
|
||||
"tools": [...] // ❌ 错误:应该使用 resources.tools
|
||||
}
|
||||
|
||||
**❌ 错误示例2**(system_features 中的配置错误):
|
||||
{
|
||||
"resources": {
|
||||
"system_features": {
|
||||
"file_upload": {
|
||||
"enabled": true
|
||||
// ❌ 错误:启用时缺少 purpose 字段
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
**严格输出规则**:
|
||||
- ❌ 不要使用 \`\`\`json 或其他代码块标记
|
||||
- ❌ 不要使用旧格式的 tools 字段,必须使用 resources 结构
|
||||
- ❌ 不要添加任何解释性文字或前言后语
|
||||
- ✅ 必须使用 resources 对象,包含 tools、knowledges、system_features
|
||||
- ✅ file_upload.enabled=true 时必须提供 purpose 字段
|
||||
- ✅ knowledges 或 tools 可以为空数组(如果不需要)
|
||||
- ✅ 直接、纯净地输出JSON内容
|
||||
|
||||
质量要求:
|
||||
1. 任务理解深度:确保分析基于对用户需求的深度理解
|
||||
2. 资源匹配精度:每个资源的选择都要有明确的理由
|
||||
3. 资源完整性:确保所有必需的资源都包含在 resources 配置中
|
||||
4. 输出格式规范:严格遵循 resources 结构要求
|
||||
5. 资源去重:同一个资源在 tools 或 knowledges 数组中只出现一次
|
||||
6. type准确性:工具的type为"tool",知识库的type为"knowledge"
|
||||
7. 系统功能配置正确:file_upload.enabled=true时必须提供purpose字段
|
||||
8. 输出纯净性:只输出JSON,不包含任何其他内容
|
||||
</plan_generation_phase>
|
||||
|
||||
<phase_transition>
|
||||
**信息收集完成条件**(满足任一即可切换到计划生成):
|
||||
- 已收集到明确的目标描述、主要约束条件和成功标准
|
||||
- 对话轮次达到6轮
|
||||
- 用户表示信息足够完整
|
||||
- 当前收集的信息已足够制定可行的计划
|
||||
|
||||
**重要:切换标识符要求**
|
||||
当判断「信息收集已完成」,准备切换到计划生成阶段时,**必须**在回复的开始处包含以下标识符:
|
||||
\`\`\`
|
||||
「信息收集已完成」
|
||||
\`\`\`
|
||||
|
||||
然后继续说:
|
||||
"基于我们刚才的交流,我已经收集到足够的信息来为您制定执行计划。现在让我根据您的需求和实际情况,生成详细的任务分解方案。"
|
||||
|
||||
**示例格式**:
|
||||
\`\`\`
|
||||
「信息收集已完成」
|
||||
|
||||
基于我们刚才的交流,我已经收集到足够的信息来为您制定执行计划...
|
||||
\`\`\`
|
||||
|
||||
这样系统会自动检测到标识符并切换到计划生成阶段。
|
||||
</phase_transition>
|
||||
|
||||
<conversation_rules>
|
||||
**回复格式**:
|
||||
- 信息收集阶段:专业、友善、充满好奇心,每轮只问1-2个核心问题
|
||||
- 计划生成阶段:直接输出JSON格式的执行计划
|
||||
|
||||
**特殊命令处理**:
|
||||
- 如果搭建者明确要求"直接生成流程",可以开始规划
|
||||
- 如果需要补充信息,可以回到信息收集
|
||||
- 避免过度询问,保持高效(3-4轮完成信息收集)
|
||||
|
||||
**质量保证**:
|
||||
- 收集的信息要具体、准确、可验证
|
||||
- 生成的计划要基于收集到的信息
|
||||
- 确保计划中的每个步骤都是可执行的
|
||||
- 严格基于系统能力边界进行规划
|
||||
</conversation_rules>`;
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from 'zod';
|
||||
import { HelperBotCompletionsParamsSchema } from '../../../../../global/openapi/core/chat/helperBot/api';
|
||||
import { HelperBotChatItemSchema } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import { WorkflowResponseFnSchema } from '../../../workflow/dispatch/type';
|
||||
|
||||
export const HelperBotDispatchParamsSchema = z.object({
|
||||
query: z.string(),
|
||||
files: HelperBotCompletionsParamsSchema.shape.files,
|
||||
metadata: HelperBotCompletionsParamsSchema.shape.metadata,
|
||||
histories: z.array(HelperBotChatItemSchema),
|
||||
workflowResponseWrite: WorkflowResponseFnSchema
|
||||
});
|
||||
export type HelperBotDispatchParamsType = z.infer<typeof HelperBotDispatchParamsSchema>;
|
||||
|
||||
export const HelperBotDispatchResponseSchema = z.object({});
|
||||
export type HelperBotDispatchResponseType = z.infer<typeof HelperBotDispatchResponseSchema>;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { connectionMongo, getMongoModel } from '../../common/mongo';
|
||||
const { Schema } = connectionMongo;
|
||||
import { type ChatSchemaType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { type ChatSchemaType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
TeamCollectionName,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type {
|
|||
AIChatItemType,
|
||||
ChatHistoryItemResType,
|
||||
UserChatItemType
|
||||
} from '@fastgpt/global/core/chat/type.d';
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { MongoApp } from '../app/schema';
|
||||
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* Abandoned */
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { type SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type';
|
||||
import { runWorkflow } from '../index';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils';
|
||||
import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
|
|
@ -316,7 +316,7 @@ async function getMultiInput({
|
|||
runningUserInfo
|
||||
}: {
|
||||
histories: ChatItemType[];
|
||||
inputFiles: UserChatItemValueItemType['file'][];
|
||||
inputFiles: UserChatItemFileItemType[];
|
||||
fileLinks?: string[];
|
||||
stringQuoteText?: string; // file quote
|
||||
requestOrigin?: string;
|
||||
|
|
@ -368,7 +368,9 @@ async function getMultiInput({
|
|||
|
||||
return {
|
||||
documentQuoteText: text,
|
||||
userFiles: fileLinks.map((url) => parseUrlToFileType(url)).filter(Boolean)
|
||||
userFiles: fileLinks
|
||||
.map((url) => parseUrlToFileType(url))
|
||||
.filter(Boolean) as UserChatItemFileItemType[]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -399,7 +401,7 @@ async function getChatMessages({
|
|||
systemPrompt: string;
|
||||
userChatInput: string;
|
||||
|
||||
userFiles: UserChatItemValueItemType['file'][];
|
||||
userFiles: UserChatItemFileItemType[];
|
||||
documentQuoteText?: string; // document quote
|
||||
}) {
|
||||
// Dataset prompt ====>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/template/system/classifyQuestion/type';
|
||||
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type';
|
||||
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import { getLLMModel } from '../../../../ai/model';
|
|||
import { filterToolNodeIdByEdges, getNodeErrResponse, getHistories } from '../../utils';
|
||||
import { runToolCall } from './toolCall';
|
||||
import { type DispatchToolModuleProps, type ToolNodeItemType } from './type';
|
||||
import { type ChatItemType, type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import type {
|
||||
UserChatItemFileItemType,
|
||||
ChatItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
GPTMessages2Chats,
|
||||
|
|
@ -121,10 +125,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
|||
fileLinks,
|
||||
inputFiles: globalFiles,
|
||||
hasReadFilesTool,
|
||||
usageId,
|
||||
appId: props.runningAppInfo.id,
|
||||
chatId: props.chatId,
|
||||
uId: props.uid
|
||||
usageId
|
||||
});
|
||||
|
||||
const concatenateSystemPrompt = [
|
||||
|
|
@ -284,10 +285,7 @@ const getMultiInput = async ({
|
|||
customPdfParse,
|
||||
inputFiles,
|
||||
hasReadFilesTool,
|
||||
usageId,
|
||||
appId,
|
||||
chatId,
|
||||
uId
|
||||
usageId
|
||||
}: {
|
||||
runningUserInfo: ChatDispatchProps['runningUserInfo'];
|
||||
histories: ChatItemType[];
|
||||
|
|
@ -295,12 +293,9 @@ const getMultiInput = async ({
|
|||
requestOrigin?: string;
|
||||
maxFiles: number;
|
||||
customPdfParse?: boolean;
|
||||
inputFiles: UserChatItemValueItemType['file'][];
|
||||
inputFiles: UserChatItemFileItemType[];
|
||||
hasReadFilesTool: boolean;
|
||||
usageId?: string;
|
||||
appId: string;
|
||||
chatId?: string;
|
||||
uId: string;
|
||||
}) => {
|
||||
// Not file quote
|
||||
if (!fileLinks || hasReadFilesTool) {
|
||||
|
|
@ -334,7 +329,9 @@ const getMultiInput = async ({
|
|||
|
||||
return {
|
||||
documentQuoteText: text,
|
||||
userFiles: fileLinks.map((url) => parseUrlToFileType(url)).filter(Boolean)
|
||||
userFiles: fileLinks
|
||||
.map((url) => parseUrlToFileType(url))
|
||||
.filter(Boolean) as UserChatItemFileItemType[]
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { runWorkflow } from '../index';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type {
|
|||
ChatHistoryItemResType,
|
||||
NodeOutputItemType,
|
||||
ToolRunResponseItemType
|
||||
} from '@fastgpt/global/core/chat/type.d';
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import type { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { NodeInputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
|
|
|||
|
|
@ -3,15 +3,18 @@ import type {
|
|||
ChatHistoryItemResType,
|
||||
ToolRunResponseItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import type {
|
||||
DispatchNodeResponseKeyEnum,
|
||||
SseResponseEventEnum
|
||||
} from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import type {
|
||||
InteractiveNodeResponseType,
|
||||
WorkflowInteractiveResponseType
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
|
||||
import z from 'zod';
|
||||
|
||||
export type WorkflowDebugResponse = {
|
||||
memoryEdges: RuntimeEdgeItemType[];
|
||||
|
|
@ -41,10 +44,16 @@ export type DispatchFlowResponse = {
|
|||
durationSeconds: number;
|
||||
};
|
||||
|
||||
export type WorkflowResponseType = (e: {
|
||||
id?: string;
|
||||
stepId?: string;
|
||||
export const WorkflowResponseFnSchema = z.function({
|
||||
input: z.tuple([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
stepId: z.string().optional(),
|
||||
event: z.custom<SseResponseEventEnum>(),
|
||||
data: z.record(z.string(), z.any())
|
||||
})
|
||||
]),
|
||||
output: z.void()
|
||||
});
|
||||
|
||||
event: SseResponseEventEnum;
|
||||
data: Record<string, any>;
|
||||
}) => void;
|
||||
export type WorkflowResponseType = z.infer<typeof WorkflowResponseFnSchema>;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { NodeOutputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import type { VariableItemType } from '@fastgpt/global/core/app/type';
|
||||
import { encryptSecret } from '../../../common/secret/aes256gcm';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
|
||||
|
||||
type PaginationProps<T = {}> = T & {
|
||||
export type PaginationProps<T = {}> = T & {
|
||||
pageSize: number | string;
|
||||
} & RequireOnlyOne<{
|
||||
offset: number | string;
|
||||
pageNum: number | string;
|
||||
}>;
|
||||
|
||||
type PaginationResponse<T = {}> = {
|
||||
export type PaginationResponse<T = {}> = {
|
||||
total: number;
|
||||
list: T[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { type ReactNode, type RefObject, useMemo, useRef, useState } from
|
|||
import { Box, type BoxProps } from '@chakra-ui/react';
|
||||
import { useToast } from './useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { type PaginationProps, type PaginationResponse } from '../common/fetch/type';
|
||||
import {
|
||||
useBoolean,
|
||||
useLockFn,
|
||||
|
|
@ -15,6 +14,7 @@ import {
|
|||
import MyBox from '../components/common/MyBox';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from './useRequest';
|
||||
import type { PaginationPropsType, PaginationResponseType } from '@fastgpt/global/openapi/type';
|
||||
|
||||
type ItemHeight<T> = (index: number, data: T) => number;
|
||||
const thresholdVal = 100;
|
||||
|
|
@ -31,8 +31,8 @@ export type ScrollListType = ({
|
|||
} & BoxProps) => React.JSX.Element;
|
||||
|
||||
export function useVirtualScrollPagination<
|
||||
TParams extends PaginationProps,
|
||||
TData extends PaginationResponse
|
||||
TParams extends PaginationPropsType,
|
||||
TData extends PaginationResponseType
|
||||
>(
|
||||
api: (data: TParams) => Promise<TData>,
|
||||
{
|
||||
|
|
@ -179,8 +179,8 @@ export function useVirtualScrollPagination<
|
|||
}
|
||||
|
||||
export function useScrollPagination<
|
||||
TParams extends PaginationProps,
|
||||
TData extends PaginationResponse
|
||||
TParams extends PaginationPropsType,
|
||||
TData extends PaginationResponseType
|
||||
>(
|
||||
api: (data: TParams) => Promise<TData>,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -103,11 +103,8 @@
|
|||
"add_new": "新增",
|
||||
"add_new_param": "新增参数",
|
||||
"add_success": "添加成功",
|
||||
<<<<<<< HEAD
|
||||
"aipoint_desc": "每次调用 AI 模型时,都会消耗一定的 AI 积分(类似于 token)。点击可查看详细计算规则。",
|
||||
"agent_prompt_tips": "建议按照以下模板填写,以获得最佳效果。\n「角色身份」\n「任务目标」\n「任务流程与技能」\n输入“/”插入全局变量;输入“@”插入特定技能,包括应用、工具、知识库、模型。",
|
||||
=======
|
||||
>>>>>>> e4b2fca6d (tool select ux)
|
||||
"all_quotes": "全部引用",
|
||||
"all_result": "完整结果",
|
||||
"app_evaluation": "Agent 评测(Beta)",
|
||||
|
|
|
|||
|
|
@ -102,11 +102,8 @@
|
|||
"add_new": "新增",
|
||||
"add_new_param": "新增參數",
|
||||
"add_success": "新增成功",
|
||||
<<<<<<< HEAD
|
||||
"aipoint_desc": "每次呼叫 AI 模型時,都會消耗一定的 AI 點數(類似於 Token)。點選可檢視詳細計算規則。",
|
||||
"agent_prompt_tips": "建議按照以下模板填寫,以獲得最佳效果。\n\n「角色身份」\n「任務目標」\n「任務流程與技能」\n輸入“/”插入全局變量;輸入“@”插入特定技能,包括應用、工具、知識庫、模型。",
|
||||
=======
|
||||
>>>>>>> e4b2fca6d (tool select ux)
|
||||
"all_quotes": "全部引用",
|
||||
"all_result": "完整結果",
|
||||
"app_evaluation": "應用評測(Beta)",
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ const ChatItem = ({ hasPlanCheck, ...props }: Props) => {
|
|||
|
||||
return [];
|
||||
}, [chat.obj, chat.value, isChatting]);
|
||||
console.log(chat.value, splitAiResponseResults, 232);
|
||||
|
||||
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
|
||||
const onOpenCiteModal = useMemoizedFn(
|
||||
(item?: {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
|||
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import DatasetSelectModal, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { type AdminFbkType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { type AdminFbkType } from '@fastgpt/global/core/chat/type';
|
||||
import SelectCollections from '@/web/core/dataset/components/SelectCollections';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
AIChatItemValueItemType,
|
||||
ChatSiteItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '@fastgpt/global/core/chat/type.d';
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { Box, Button, Checkbox, Flex } from '@chakra-ui/react';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,400 @@
|
|||
import type { FlexProps } from '@chakra-ui/react';
|
||||
import { Box, Flex, Textarea, useBoolean } from '@chakra-ui/react';
|
||||
import React, { useRef, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
type ChatBoxInputFormType,
|
||||
type ChatBoxInputType,
|
||||
type SendPromptFnType
|
||||
} from '../ChatContainer/ChatBox/type';
|
||||
import { textareaMinH } from '../ChatContainer/ChatBox/constants';
|
||||
import { useFieldArray, type UseFormReturn } from 'react-hook-form';
|
||||
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowRuntimeContext } from '../ChatContainer/context/workflowRuntimeContext';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { documentFileType } from '@fastgpt/global/common/file/constants';
|
||||
import FilePreview from '../ChatContainer/components/FilePreview';
|
||||
import { useFileUpload } from './hooks/useFileUpload';
|
||||
import ComplianceTip from '@/components/common/ComplianceTip/index';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { HelperBotContext } from './context';
|
||||
import type { onSendMessageFnType } from './type';
|
||||
|
||||
const fileTypeFilter = (file: File) => {
|
||||
return (
|
||||
file.type.includes('image') ||
|
||||
documentFileType.split(',').some((type) => file.name.endsWith(type.trim()))
|
||||
);
|
||||
};
|
||||
|
||||
const ChatInput = ({
|
||||
chatId,
|
||||
onSendMessage,
|
||||
onStop,
|
||||
TextareaDom,
|
||||
chatForm,
|
||||
isChatting
|
||||
}: {
|
||||
chatId: string;
|
||||
onSendMessage: onSendMessageFnType;
|
||||
onStop: () => void;
|
||||
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
chatForm: UseFormReturn<ChatBoxInputFormType>;
|
||||
isChatting: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const { setValue, watch, control } = chatForm;
|
||||
const inputValue = watch('input');
|
||||
|
||||
const type = useContextSelector(HelperBotContext, (v) => v.type);
|
||||
const fileSelectConfig = useContextSelector(HelperBotContext, (v) => v.fileSelectConfig);
|
||||
|
||||
const [focusing, { on: onFocus, off: offFocus }] = useBoolean();
|
||||
|
||||
const fileCtrl = useFieldArray({
|
||||
control,
|
||||
name: 'files'
|
||||
});
|
||||
const {
|
||||
File,
|
||||
onOpenSelectFile,
|
||||
fileList,
|
||||
onSelectFile,
|
||||
selectFileIcon,
|
||||
selectFileLabel,
|
||||
showSelectFile,
|
||||
showSelectImg,
|
||||
showSelectVideo,
|
||||
showSelectAudio,
|
||||
showSelectCustomFileExtension,
|
||||
removeFiles,
|
||||
replaceFiles,
|
||||
hasFileUploading
|
||||
} = useFileUpload({
|
||||
fileSelectConfig,
|
||||
fileCtrl,
|
||||
type,
|
||||
chatId
|
||||
});
|
||||
const havInput = !!inputValue || fileList.length > 0;
|
||||
const canSendMessage = havInput && !hasFileUploading;
|
||||
const canUploadFile =
|
||||
showSelectFile ||
|
||||
showSelectImg ||
|
||||
showSelectVideo ||
|
||||
showSelectAudio ||
|
||||
showSelectCustomFileExtension;
|
||||
|
||||
/* on send */
|
||||
const handleSend = useCallback(
|
||||
async (val?: string) => {
|
||||
if (!canSendMessage) return;
|
||||
const textareaValue = val || TextareaDom.current?.value || '';
|
||||
|
||||
onSendMessage({
|
||||
query: textareaValue.trim(),
|
||||
files: fileList
|
||||
});
|
||||
replaceFiles([]);
|
||||
},
|
||||
[TextareaDom, canSendMessage, fileList, onSendMessage, replaceFiles]
|
||||
);
|
||||
|
||||
const RenderTextarea = useMemo(
|
||||
() => (
|
||||
<Flex direction={'column'} mt={fileList.length > 0 ? 1 : 0}>
|
||||
{/* Textarea */}
|
||||
<Flex w={'100%'}>
|
||||
{/* Prompt Container */}
|
||||
<Textarea
|
||||
ref={TextareaDom}
|
||||
py={0}
|
||||
mx={[2, 4]}
|
||||
px={2}
|
||||
border={'none'}
|
||||
_focusVisible={{
|
||||
border: 'none'
|
||||
}}
|
||||
placeholder={
|
||||
isPc ? t('common:core.chat.Type a message') : t('chat:input_placeholder_phone')
|
||||
}
|
||||
resize={'none'}
|
||||
rows={1}
|
||||
height={[5, 6]}
|
||||
lineHeight={[5, 6]}
|
||||
maxHeight={[24, 32]}
|
||||
minH={'50px'}
|
||||
mb={0}
|
||||
maxLength={-1}
|
||||
overflowY={'hidden'}
|
||||
overflowX={'hidden'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-word'}
|
||||
boxShadow={'none !important'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={400}
|
||||
fontSize={'1rem'}
|
||||
letterSpacing={'0.5px'}
|
||||
w={'100%'}
|
||||
_placeholder={{
|
||||
color: '#707070',
|
||||
fontSize: 'sm'
|
||||
}}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
textarea.style.height = textareaMinH;
|
||||
const maxHeight = 128;
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
|
||||
// Only show scrollbar when content exceeds max height
|
||||
if (textarea.scrollHeight > maxHeight) {
|
||||
textarea.style.overflowY = 'auto';
|
||||
} else {
|
||||
textarea.style.overflowY = 'hidden';
|
||||
}
|
||||
|
||||
setValue('input', textarea.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// enter send.(pc or iframe && enter and unPress shift)
|
||||
const isEnter = e.key === 'Enter';
|
||||
if (isEnter && TextareaDom.current && (e.ctrlKey || e.altKey)) {
|
||||
// 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 = `${TextareaDom.current.scrollHeight}px`;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select all content
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
|
||||
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
|
||||
handleSend();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onPaste={(e) => {
|
||||
const clipboardData = e.clipboardData;
|
||||
if (clipboardData && canUploadFile) {
|
||||
const items = clipboardData.items;
|
||||
const files = Array.from(items)
|
||||
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
|
||||
.filter((file) => {
|
||||
return file && fileTypeFilter(file);
|
||||
}) as File[];
|
||||
onSelectFile({ files });
|
||||
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFocus={onFocus}
|
||||
onBlur={offFocus}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
[
|
||||
fileList.length,
|
||||
TextareaDom,
|
||||
isPc,
|
||||
t,
|
||||
inputValue,
|
||||
onFocus,
|
||||
offFocus,
|
||||
setValue,
|
||||
handleSend,
|
||||
canUploadFile,
|
||||
onSelectFile
|
||||
]
|
||||
);
|
||||
|
||||
const RenderButtonGroup = useMemo(() => {
|
||||
const iconSize = {
|
||||
w: isPc ? '20px' : '16px',
|
||||
h: isPc ? '20px' : '16px'
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
alignItems={'flex-start'}
|
||||
justifyContent={'space-between'}
|
||||
w={'100%'}
|
||||
mt={0}
|
||||
pr={[3, 4]}
|
||||
pl={[3, 4]}
|
||||
h={[8, 9]}
|
||||
gap={[0, 1]}
|
||||
>
|
||||
<Box flex={1} />
|
||||
{/* Right button group */}
|
||||
<Flex alignItems={'center'} gap={[0, 1]}>
|
||||
{/* Attachment Group */}
|
||||
<Flex alignItems={'center'} h={[8, 9]}>
|
||||
{/* file selector button */}
|
||||
{canUploadFile && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
w={[8, 9]}
|
||||
h={[8, 9]}
|
||||
p={[1, 2]}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'rgba(0, 0, 0, 0.04)' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenSelectFile();
|
||||
}}
|
||||
>
|
||||
<MyTooltip label={selectFileLabel}>
|
||||
<MyIcon name={selectFileIcon as any} {...iconSize} color={'#707070'} />
|
||||
</MyTooltip>
|
||||
<File onSelect={(files) => onSelectFile({ files })} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* Divider Container */}
|
||||
{canUploadFile && (
|
||||
<Flex alignItems={'center'} justifyContent={'center'} w={2} h={4} mr={2}>
|
||||
<Box w={'2px'} h={5} bg={'myGray.200'} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* Send Button Container */}
|
||||
<Flex alignItems={'center'} w={[8, 9]} h={[8, 9]} borderRadius={'lg'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
w={[7, 9]}
|
||||
h={[7, 9]}
|
||||
p={[1, 2]}
|
||||
bg={
|
||||
isChatting ? 'primary.50' : canSendMessage ? 'primary.500' : 'rgba(17, 24, 36, 0.1)'
|
||||
}
|
||||
borderRadius={['md', 'lg']}
|
||||
cursor={isChatting ? 'pointer' : canSendMessage ? 'pointer' : 'not-allowed'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isChatting) {
|
||||
return onStop();
|
||||
}
|
||||
return handleSend();
|
||||
}}
|
||||
>
|
||||
{isChatting ? (
|
||||
<MyIcon {...iconSize} name={'stop'} color={'primary.600'} />
|
||||
) : (
|
||||
<MyTooltip label={t('common:core.chat.Send Message')}>
|
||||
<MyIcon name={'core/chat/sendFill'} {...iconSize} color={'white'} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}, [
|
||||
isPc,
|
||||
canUploadFile,
|
||||
selectFileLabel,
|
||||
selectFileIcon,
|
||||
File,
|
||||
isChatting,
|
||||
canSendMessage,
|
||||
t,
|
||||
onOpenSelectFile,
|
||||
onSelectFile,
|
||||
handleSend,
|
||||
onStop
|
||||
]);
|
||||
|
||||
const activeStyles: FlexProps = {
|
||||
boxShadow: '0px 5px 20px -4px rgba(19, 51, 107, 0.13)',
|
||||
border: '0.5px solid rgba(0, 0, 0, 0.24)'
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!canUploadFile) return;
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
|
||||
const droppedFiles = files.filter((file) => fileTypeFilter(file));
|
||||
if (droppedFiles.length > 0) {
|
||||
onSelectFile({ files: droppedFiles });
|
||||
}
|
||||
|
||||
const invalidFileName = files
|
||||
.filter((file) => !fileTypeFilter(file))
|
||||
.map((file) => file.name)
|
||||
.join(', ');
|
||||
if (invalidFileName) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('chat:unsupported_file_type'),
|
||||
description: invalidFileName
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Real Chat Input */}
|
||||
<Flex
|
||||
direction={'column'}
|
||||
minH={['96px', '120px']}
|
||||
pt={fileList.length > 0 ? '0' : [3, 4]}
|
||||
pb={3}
|
||||
position={'relative'}
|
||||
borderRadius={['xl', 'xxl']}
|
||||
bg={'white'}
|
||||
overflow={'display'}
|
||||
{...(focusing
|
||||
? activeStyles
|
||||
: {
|
||||
_hover: activeStyles,
|
||||
border: '0.5px solid rgba(0, 0, 0, 0.18)',
|
||||
boxShadow: `0px 5px 16px -4px rgba(19, 51, 107, 0.08)`
|
||||
})}
|
||||
onClick={() => TextareaDom?.current?.focus()}
|
||||
>
|
||||
<Box flex={1}>
|
||||
{/* file preview */}
|
||||
<Box px={[2, 3]}>
|
||||
<FilePreview fileList={fileList} removeFiles={removeFiles} />
|
||||
</Box>
|
||||
|
||||
{RenderTextarea}
|
||||
</Box>
|
||||
|
||||
<Box>{RenderButtonGroup}</Box>
|
||||
</Flex>
|
||||
<ComplianceTip type={'chat'} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ChatInput);
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { POST, GET, DELETE } from '@/web/common/api/request';
|
||||
import type {
|
||||
GetHelperBotChatRecordsParamsType,
|
||||
DeleteHelperBotChatParamsType,
|
||||
GetHelperBotChatRecordsResponseType,
|
||||
GetHelperBotFilePresignParamsType,
|
||||
GetHelperBotFilePreviewParamsType
|
||||
} from '@fastgpt/global/openapi/core/chat/helperBot/api';
|
||||
import type { CreatePostPresignedUrlResult } from '@fastgpt/service/common/s3/type';
|
||||
|
||||
export const getHelperBotChatRecords = (data: GetHelperBotChatRecordsParamsType) =>
|
||||
GET<GetHelperBotChatRecordsResponseType>('/core/chat/helperBot/getRecords', data);
|
||||
|
||||
export const deleteHelperBotChatRecord = (data: DeleteHelperBotChatParamsType) =>
|
||||
DELETE('/core/chat/helperBot/deleteRecord', data);
|
||||
|
||||
export const getHelperBotFilePresign = (data: GetHelperBotFilePresignParamsType) =>
|
||||
POST<CreatePostPresignedUrlResult>('/core/chat/helperBot/getFilePresign', data);
|
||||
|
||||
export const getHelperBotFilePreview = (data: GetHelperBotFilePreviewParamsType) =>
|
||||
POST<string>('/core/chat/helperBot/getFilePreview', data);
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import type { HelperBotChatItemSiteType } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import {
|
||||
Box,
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Button,
|
||||
Flex,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import AIResponseBox from '../../components/AIResponseBox';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
|
||||
const accordionButtonStyle = {
|
||||
w: 'auto',
|
||||
bg: 'white',
|
||||
borderRadius: 'md',
|
||||
borderWidth: '1px',
|
||||
borderColor: 'myGray.200',
|
||||
boxShadow: '1',
|
||||
pl: 3,
|
||||
pr: 2.5,
|
||||
_hover: {
|
||||
bg: 'auto'
|
||||
}
|
||||
};
|
||||
const RenderResoningContent = React.memo(function RenderResoningContent({
|
||||
content,
|
||||
isChatting,
|
||||
isLastResponseValue
|
||||
}: {
|
||||
content: string;
|
||||
isChatting: boolean;
|
||||
isLastResponseValue: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const showAnimation = isChatting && isLastResponseValue;
|
||||
|
||||
return (
|
||||
<Accordion allowToggle defaultIndex={isLastResponseValue ? 0 : undefined}>
|
||||
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||
<AccordionButton {...accordionButtonStyle} py={1}>
|
||||
<HStack mr={2} spacing={1}>
|
||||
<MyIcon name={'core/chat/think'} w={'0.85rem'} />
|
||||
<Box fontSize={'sm'}>{t('chat:ai_reasoning')}</Box>
|
||||
</HStack>
|
||||
|
||||
{showAnimation && <MyIcon name={'common/loading'} w={'0.85rem'} />}
|
||||
<AccordionIcon color={'myGray.600'} ml={5} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel
|
||||
py={0}
|
||||
pr={0}
|
||||
pl={3}
|
||||
mt={2}
|
||||
borderLeft={'2px solid'}
|
||||
borderColor={'myGray.300'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Markdown source={content} showAnimation={showAnimation} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
});
|
||||
const RenderText = React.memo(function RenderText({
|
||||
showAnimation,
|
||||
text
|
||||
}: {
|
||||
showAnimation: boolean;
|
||||
text: string;
|
||||
}) {
|
||||
const source = useMemo(() => {
|
||||
if (!text) return '';
|
||||
|
||||
// Remove quote references if not showing response detail
|
||||
return text;
|
||||
}, [text]);
|
||||
|
||||
return <Markdown source={source} showAnimation={showAnimation} />;
|
||||
});
|
||||
|
||||
const AIItem = ({
|
||||
chat,
|
||||
isChatting,
|
||||
isLastChild
|
||||
}: {
|
||||
chat: HelperBotChatItemSiteType;
|
||||
isChatting: boolean;
|
||||
isLastChild: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
return (
|
||||
<Box
|
||||
_hover={{
|
||||
'& .controler': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'sm'}
|
||||
display="inline-block"
|
||||
maxW={['calc(100% - 25px)', 'calc(100% - 40px)']}
|
||||
color={'myGray.900'}
|
||||
bg={'myGray.100'}
|
||||
>
|
||||
{chat.value.map((value, i) => {
|
||||
if ('text' in value && value.text) {
|
||||
return (
|
||||
<RenderText
|
||||
key={i}
|
||||
showAnimation={isChatting && isLastChild}
|
||||
text={value.text.content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if ('reasoning' in value && value.reasoning) {
|
||||
return (
|
||||
<RenderResoningContent
|
||||
key={i}
|
||||
isChatting={isChatting}
|
||||
isLastResponseValue={isLastChild}
|
||||
content={value.reasoning.content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
{/* Controller */}
|
||||
<Flex h={'26px'} mt={1}>
|
||||
<Flex className="controler" display={['flex', 'none']} alignItems={'center'} gap={1}>
|
||||
<MyTooltip label={t('common:Copy')}>
|
||||
<MyIconButton
|
||||
icon="copy"
|
||||
color={'myGray.500'}
|
||||
onClick={() => {
|
||||
const text = chat.value
|
||||
.map((value) => {
|
||||
if ('text' in value) {
|
||||
return value.text || '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
return copyData(text ?? '');
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('common:Delete')}>
|
||||
<MyIconButton
|
||||
icon="delete"
|
||||
color={'myGray.500'}
|
||||
hoverColor={'red.600'}
|
||||
hoverBg={'red.50'}
|
||||
// onClick={() => copyData(text ?? '')}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIItem;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import type { HelperBotChatItemSiteType } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import { formatChatValue2InputType } from '../../ChatContainer/ChatBox/utils';
|
||||
import { Box, Card, Flex } from '@chakra-ui/react';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import FileBlock from '../../ChatContainer/ChatBox/components/FilesBox';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import IconButton from '@/pageComponents/account/team/OrgManage/IconButton';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
|
||||
const HumanItem = ({ chat }: { chat: HelperBotChatItemSiteType }) => {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const { text, files = [] } = formatChatValue2InputType(chat.value);
|
||||
|
||||
// TODO: delete chatitem
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction={'column'}
|
||||
alignItems={'end'}
|
||||
_hover={{
|
||||
'& .controler': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'sm'}
|
||||
display="inline-block"
|
||||
textAlign="right"
|
||||
maxW={['calc(100% - 25px)', 'calc(100% - 40px)']}
|
||||
color={'myGray.900'}
|
||||
bg={'primary.100'}
|
||||
order={0}
|
||||
>
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
{files.length > 0 && <FileBlock files={files} />}
|
||||
{text && <Markdown source={text} />}
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* Controller */}
|
||||
<Flex h={'26px'} mt={1}>
|
||||
<Flex className="controler" display={['flex', 'none']} alignItems={'center'} gap={1}>
|
||||
<MyTooltip label={t('common:Copy')}>
|
||||
<MyIconButton icon="copy" color={'myGray.500'} onClick={() => copyData(text ?? '')} />
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('common:Delete')}>
|
||||
<MyIconButton
|
||||
icon="delete"
|
||||
color={'myGray.500'}
|
||||
hoverColor={'red.600'}
|
||||
hoverBg={'red.50'}
|
||||
onClick={() => copyData(text ?? '')}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default HumanItem;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
|
||||
import React, { useState, type ReactNode } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import {
|
||||
HelperBotTypeEnum,
|
||||
type HelperBotTypeEnumType,
|
||||
type TopAgentParamsType
|
||||
} from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
|
||||
|
||||
export type HelperBotProps = {
|
||||
emptyDom?: ReactNode;
|
||||
fileSelectConfig?: AppFileSelectConfigType;
|
||||
} & {
|
||||
type: HelperBotTypeEnumType;
|
||||
metadata: TopAgentParamsType;
|
||||
onApply: (e: TopAgentParamsType) => void;
|
||||
};
|
||||
type HelperBotContextType = HelperBotProps & {};
|
||||
|
||||
export const HelperBotContext = createContext<HelperBotContextType>({
|
||||
type: HelperBotTypeEnum.topAgent,
|
||||
metadata: {
|
||||
role: '',
|
||||
taskObject: '',
|
||||
selectedTools: [],
|
||||
selectedDatasets: [],
|
||||
fileUpload: false
|
||||
},
|
||||
onApply: function (e: TopAgentParamsType): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
const HelperBotContextProvider = ({
|
||||
children,
|
||||
...params
|
||||
}: { children: ReactNode } & HelperBotProps) => {
|
||||
const contextValue: HelperBotContextType = useMemoEnhance(() => params, [params]);
|
||||
return <HelperBotContext.Provider value={contextValue}>{children}</HelperBotContext.Provider>;
|
||||
};
|
||||
export default HelperBotContextProvider;
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { getFileIcon } from '@fastgpt/global/common/file/icon';
|
||||
import { formatFileSize } from '@fastgpt/global/common/file/tools';
|
||||
import { clone } from 'lodash';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { type UseFieldArrayReturn } from 'react-hook-form';
|
||||
import type { ChatBoxInputFormType, UserInputFileItemType } from '../../ChatContainer/ChatBox/type';
|
||||
import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { getPresignedChatFileGetUrl, getUploadChatFilePresignedUrl } from '@/web/common/file/api';
|
||||
import { POST } from '@/web/common/api/request';
|
||||
import { getUploadFileType } from '@fastgpt/global/core/app/constants';
|
||||
import { parseS3UploadError } from '@fastgpt/global/common/error/s3';
|
||||
import type { HelperBotTypeEnumType } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import { getHelperBotFilePresign, getHelperBotFilePreview } from '../api';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
type UseFileUploadOptions = {
|
||||
fileSelectConfig?: AppFileSelectConfigType;
|
||||
fileCtrl: UseFieldArrayReturn<ChatBoxInputFormType, 'files', 'id'>;
|
||||
|
||||
type: HelperBotTypeEnumType;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
export const useFileUpload = (props: UseFileUploadOptions) => {
|
||||
const { fileSelectConfig, fileCtrl, type, chatId } = props;
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const {
|
||||
update: updateFiles,
|
||||
remove: removeFiles,
|
||||
fields: fileList,
|
||||
replace: replaceFiles,
|
||||
append: appendFiles
|
||||
} = fileCtrl;
|
||||
const hasFileUploading = fileList.some((item) => !item.url);
|
||||
|
||||
const showSelectFile = fileSelectConfig?.canSelectFile;
|
||||
const showSelectImg = fileSelectConfig?.canSelectImg;
|
||||
const showSelectVideo = fileSelectConfig?.canSelectVideo;
|
||||
const showSelectAudio = fileSelectConfig?.canSelectAudio;
|
||||
const showSelectCustomFileExtension = fileSelectConfig?.canSelectCustomFileExtension;
|
||||
const canUploadFile =
|
||||
showSelectFile ||
|
||||
showSelectImg ||
|
||||
showSelectVideo ||
|
||||
showSelectAudio ||
|
||||
showSelectCustomFileExtension;
|
||||
const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10;
|
||||
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
|
||||
const canSelectFileAmount = maxSelectFiles - fileList.length;
|
||||
|
||||
const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => {
|
||||
if (canUploadFile) {
|
||||
return {
|
||||
icon: 'core/chat/fileSelect',
|
||||
label: t('chat:select_file')
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}, [canUploadFile, t]);
|
||||
|
||||
const fileType = useMemo(() => {
|
||||
return getUploadFileType({
|
||||
canSelectFile: showSelectFile,
|
||||
canSelectImg: showSelectImg,
|
||||
canSelectVideo: showSelectVideo,
|
||||
canSelectAudio: showSelectAudio,
|
||||
canSelectCustomFileExtension: showSelectCustomFileExtension,
|
||||
customFileExtensionList: fileSelectConfig?.customFileExtensionList
|
||||
});
|
||||
}, [
|
||||
fileSelectConfig?.customFileExtensionList,
|
||||
showSelectAudio,
|
||||
showSelectCustomFileExtension,
|
||||
showSelectFile,
|
||||
showSelectImg,
|
||||
showSelectVideo
|
||||
]);
|
||||
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType,
|
||||
multiple: true,
|
||||
maxCount: canSelectFileAmount
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async ({ files }: { files: File[] }) => {
|
||||
if (!files || files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter max files
|
||||
if (files.length > maxSelectFiles) {
|
||||
files = files.slice(0, maxSelectFiles);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('chat:file_amount_over', { max: maxSelectFiles })
|
||||
});
|
||||
}
|
||||
|
||||
// Filter files by max size
|
||||
const filterFilesByMaxSize = files.filter((file) => file.size <= maxSize);
|
||||
if (filterFilesByMaxSize.length < files.length) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('file:some_file_size_exceeds_limit', { maxSize: formatFileSize(maxSize) })
|
||||
});
|
||||
}
|
||||
|
||||
// Convert files to UserInputFileItemType
|
||||
const loadFiles = await Promise.all(
|
||||
filterFilesByMaxSize.map(
|
||||
(file) =>
|
||||
new Promise<UserInputFileItemType>((resolve, reject) => {
|
||||
if (file.type.includes('image')) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const item: UserInputFileItemType = {
|
||||
id: getNanoid(6),
|
||||
rawFile: file,
|
||||
type: ChatFileTypeEnum.image,
|
||||
name: file.name,
|
||||
icon: reader.result as string,
|
||||
status: 0
|
||||
};
|
||||
resolve(item);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject(reader.error);
|
||||
};
|
||||
} else {
|
||||
resolve({
|
||||
id: getNanoid(6),
|
||||
rawFile: file,
|
||||
type: ChatFileTypeEnum.file,
|
||||
name: file.name,
|
||||
icon: getFileIcon(file.name),
|
||||
status: 0
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
appendFiles(loadFiles);
|
||||
|
||||
return loadFiles;
|
||||
},
|
||||
[maxSelectFiles, appendFiles, toast, t, maxSize]
|
||||
);
|
||||
|
||||
const uploadFiles = useCallback(async () => {
|
||||
const filterFiles = fileList.filter((item) => item.status === 0);
|
||||
|
||||
if (filterFiles.length === 0) return;
|
||||
|
||||
replaceFiles(fileList.map((item) => ({ ...item, status: 1 })));
|
||||
let errorFileIndex: number[] = [];
|
||||
|
||||
await Promise.allSettled(
|
||||
filterFiles.map(async (file) => {
|
||||
const copyFile = clone(file);
|
||||
copyFile.status = 1;
|
||||
if (!copyFile.rawFile) return;
|
||||
|
||||
try {
|
||||
const fileIndex = fileList.findIndex((item) => item.id === file.id)!;
|
||||
|
||||
// Get Upload Post Presigned URL
|
||||
const { url, fields, maxSize } = await getHelperBotFilePresign({
|
||||
type,
|
||||
chatId,
|
||||
filename: copyFile.rawFile.name
|
||||
});
|
||||
|
||||
// Upload File to S3
|
||||
const formData = new FormData();
|
||||
Object.entries(fields).forEach(([k, v]) => formData.set(k, v));
|
||||
formData.set('file', copyFile.rawFile);
|
||||
await POST(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
},
|
||||
onUploadProgress: (e) => {
|
||||
if (!e.total) return;
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
copyFile.process = percent;
|
||||
updateFiles(fileIndex, copyFile);
|
||||
}
|
||||
}).catch((error) => Promise.reject(parseS3UploadError({ t, error, maxSize })));
|
||||
|
||||
const previewUrl = await getHelperBotFilePreview({
|
||||
key: fields.key
|
||||
});
|
||||
|
||||
// Update file url and key
|
||||
copyFile.url = previewUrl;
|
||||
copyFile.key = fields.key;
|
||||
updateFiles(fileIndex, copyFile);
|
||||
} catch (error) {
|
||||
errorFileIndex.push(fileList.findIndex((item) => item.id === file.id)!);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t(
|
||||
getErrText(error, t('common:error.upload_file_error_filename', { name: file.name }))
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
removeFiles(errorFileIndex);
|
||||
}, [chatId, fileList, removeFiles, replaceFiles, t, toast, type, updateFiles]);
|
||||
|
||||
const sortFileList = useMemo(() => {
|
||||
// Sort: Document, image
|
||||
const sortResult = clone(fileList).sort((a, b) => {
|
||||
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
|
||||
return 1;
|
||||
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return sortResult;
|
||||
}, [fileList]);
|
||||
|
||||
// Upload files
|
||||
useRequest2(uploadFiles, {
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList, type, chatId]
|
||||
});
|
||||
|
||||
return {
|
||||
File,
|
||||
onOpenSelectFile,
|
||||
fileList: sortFileList,
|
||||
onSelectFile,
|
||||
uploadFiles,
|
||||
selectFileIcon,
|
||||
selectFileLabel,
|
||||
showSelectFile,
|
||||
showSelectImg,
|
||||
showSelectVideo,
|
||||
showSelectAudio,
|
||||
showSelectCustomFileExtension,
|
||||
removeFiles,
|
||||
replaceFiles,
|
||||
hasFileUploading
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
import React, { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import HelperBotContextProvider, { type HelperBotProps } from './context';
|
||||
import type {
|
||||
AIChatItemValueItemType,
|
||||
ChatItemType,
|
||||
ChatSiteItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import type { getPaginationRecordsBody } from '@/pages/api/core/chat/getPaginationRecords';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import type { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import HumanItem from './components/HumanItem';
|
||||
import AIItem from './components/AIItem';
|
||||
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
|
||||
import { getHelperBotChatRecords } from './api';
|
||||
import type {
|
||||
GetHelperBotChatRecordsParamsType,
|
||||
GetHelperBotChatRecordsResponseType
|
||||
} from '@fastgpt/global/openapi/core/chat/helperBot/api';
|
||||
import ChatInput from './Chatinput';
|
||||
import type { ChatBoxInputFormType } from '../ChatContainer/ChatBox/type';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemoizedFn, useThrottleFn } from 'ahooks';
|
||||
import type { HelperBotChatItemSiteType } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import type { onSendMessageParamsType } from './type';
|
||||
import { textareaMinH } from '../ChatContainer/ChatBox/constants';
|
||||
import { streamFetch } from '@/web/common/api/fetch';
|
||||
import type { generatingMessageProps } from '../ChatContainer/type';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
|
||||
const ChatBox = ({ type, metadata, ...props }: HelperBotProps) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ScrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
// Messages 管理
|
||||
const [chatId, setChatId] = useState<string>(getNanoid(12));
|
||||
const [isChatting, setIsChatting] = useState(false);
|
||||
const chatForm = useForm<ChatBoxInputFormType>({
|
||||
defaultValues: {
|
||||
input: '',
|
||||
files: [],
|
||||
chatStarted: false,
|
||||
variables: {}
|
||||
}
|
||||
});
|
||||
const { setValue } = chatForm;
|
||||
|
||||
const requestParams = useMemoEnhance(() => {
|
||||
return {
|
||||
chatId,
|
||||
type,
|
||||
metadata
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
|
||||
setTimeout(() => {
|
||||
if (!ScrollContainerRef.current) {
|
||||
setTimeout(() => {
|
||||
scrollToBottom(behavior);
|
||||
}, 500);
|
||||
} else {
|
||||
ScrollContainerRef.current.scrollTo({
|
||||
top: ScrollContainerRef.current.scrollHeight,
|
||||
behavior
|
||||
});
|
||||
}
|
||||
}, delay);
|
||||
}, []);
|
||||
const {
|
||||
data: chatRecords,
|
||||
setData: setChatRecords,
|
||||
ScrollData
|
||||
} = useScrollPagination(
|
||||
async (
|
||||
data: GetHelperBotChatRecordsParamsType
|
||||
): Promise<GetHelperBotChatRecordsResponseType> => {
|
||||
const res = await getHelperBotChatRecords(data);
|
||||
|
||||
// First load scroll to bottom
|
||||
if (Number(data.offset) === 0) {
|
||||
scrollToBottom('auto');
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
list: res.list
|
||||
};
|
||||
},
|
||||
{
|
||||
pageSize: 10,
|
||||
refreshDeps: [requestParams],
|
||||
params: requestParams,
|
||||
scrollLoadType: 'top',
|
||||
showErrorToast: false
|
||||
}
|
||||
);
|
||||
|
||||
const chatController = useRef(new AbortController());
|
||||
const abortRequest = useMemoizedFn((signal: string = 'stop') => {
|
||||
chatController.current?.abort(signal);
|
||||
});
|
||||
|
||||
const resetInputVal = useMemoizedFn(({ query = '', files = [] }: onSendMessageParamsType) => {
|
||||
if (!TextareaDom.current) return;
|
||||
setValue('files', files);
|
||||
setValue('input', query);
|
||||
|
||||
sessionStorage.removeItem(`chatInput_${chatId}`);
|
||||
|
||||
setTimeout(() => {
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height =
|
||||
query === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Text area
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Message request
|
||||
const { run: generatingScroll } = useThrottleFn(
|
||||
(force?: boolean) => {
|
||||
if (!ScrollContainerRef.current) return;
|
||||
const isBottom =
|
||||
ScrollContainerRef.current.scrollTop + ScrollContainerRef.current.clientHeight + 150 >=
|
||||
ScrollContainerRef.current.scrollHeight;
|
||||
|
||||
if (isBottom || force) {
|
||||
scrollToBottom('auto');
|
||||
}
|
||||
},
|
||||
{
|
||||
wait: 100
|
||||
}
|
||||
);
|
||||
const generatingMessage = useMemoizedFn(
|
||||
({ event, text = '', reasoningText, tool }: generatingMessageProps) => {
|
||||
setChatRecords((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
if (item.obj !== ChatRoleEnum.AI) return item;
|
||||
|
||||
const updateIndex = item.value.length - 1;
|
||||
const updateValue: AIChatItemValueItemType = item.value[updateIndex];
|
||||
|
||||
if (event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) {
|
||||
if (reasoningText) {
|
||||
if (updateValue?.reasoning) {
|
||||
updateValue.reasoning.content += reasoningText;
|
||||
return {
|
||||
...item,
|
||||
value: [
|
||||
...item.value.slice(0, updateIndex),
|
||||
updateValue,
|
||||
...item.value.slice(updateIndex + 1)
|
||||
]
|
||||
};
|
||||
} else {
|
||||
const val: AIChatItemValueItemType = {
|
||||
reasoning: {
|
||||
content: reasoningText
|
||||
}
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
value: [...item.value, val]
|
||||
};
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
if (updateValue?.text) {
|
||||
updateValue.text.content += text;
|
||||
return {
|
||||
...item,
|
||||
value: [
|
||||
...item.value.slice(0, updateIndex),
|
||||
updateValue,
|
||||
...item.value.slice(updateIndex + 1)
|
||||
]
|
||||
};
|
||||
} else {
|
||||
const newValue: AIChatItemValueItemType = {
|
||||
text: {
|
||||
content: text
|
||||
}
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
value: item.value.concat(newValue)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tool call
|
||||
if (event === SseResponseEventEnum.toolCall && tool) {
|
||||
const val: AIChatItemValueItemType = {
|
||||
tool: {
|
||||
...tool,
|
||||
response: ''
|
||||
}
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
value: [...item.value, val]
|
||||
};
|
||||
}
|
||||
if (event === SseResponseEventEnum.toolParams && tool && updateValue?.tool) {
|
||||
if (tool.params) {
|
||||
updateValue.tool.params += tool.params;
|
||||
return {
|
||||
...item,
|
||||
value: [
|
||||
...item.value.slice(0, updateIndex),
|
||||
updateValue,
|
||||
...item.value.slice(updateIndex + 1)
|
||||
]
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}
|
||||
if (event === SseResponseEventEnum.toolResponse && tool && updateValue?.tool) {
|
||||
if (tool.response) {
|
||||
// replace tool response
|
||||
updateValue.tool.response += tool.response;
|
||||
|
||||
return {
|
||||
...item,
|
||||
value: [
|
||||
...item.value.slice(0, updateIndex),
|
||||
updateValue,
|
||||
...item.value.slice(updateIndex + 1)
|
||||
]
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
generatingScroll(false);
|
||||
}
|
||||
);
|
||||
const handleSendMessage = useMemoizedFn(async ({ query = '' }: onSendMessageParamsType) => {
|
||||
// Init check
|
||||
if (isChatting) {
|
||||
return toast({
|
||||
title: t('chat:is_chatting'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
abortRequest();
|
||||
query = query.trim();
|
||||
|
||||
if (!query) {
|
||||
toast({
|
||||
title: t('chat:content_empty'),
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const chatItemDataId = getNanoid(24);
|
||||
const newChatList: HelperBotChatItemSiteType[] = [
|
||||
...chatRecords,
|
||||
{
|
||||
_id: getNanoid(24),
|
||||
createTime: new Date(),
|
||||
dataId: chatItemDataId,
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: [
|
||||
{
|
||||
text: {
|
||||
content: query
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
_id: getNanoid(24),
|
||||
createTime: new Date(),
|
||||
dataId: chatItemDataId,
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: [
|
||||
{
|
||||
text: {
|
||||
content: `我无法直接通过“读取静态网页工具”获取 GitHub(动态站点)上的实时信息,因此不能自动抓取 fastgpt 的 star 数量。
|
||||
|
||||
不过你可以告诉我:
|
||||
|
||||
- 你指的是 **FastGPT(fastgpt-dev/FastGPT)** 吗?
|
||||
GitHub 链接通常是:https://github.com/fastgpt-dev/FastGPT
|
||||
|
||||
如果是这个项目,我可以根据我最新的训练数据给你一个**大致的历史 star 数范围**,或者你也可以让我协助编写脚本来实时查询它的 star。
|
||||
|
||||
你希望我:
|
||||
|
||||
1. 提供现阶段近似 star 数(基于 2025 的大致趋势)?
|
||||
2. 帮你写一个脚本实时查 GitHub API?
|
||||
3. 还是提供该项目的介绍?`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
setChatRecords(newChatList);
|
||||
|
||||
resetInputVal({});
|
||||
scrollToBottom();
|
||||
|
||||
setIsChatting(true);
|
||||
try {
|
||||
const abortSignal = new AbortController();
|
||||
chatController.current = abortSignal;
|
||||
|
||||
const { responseText } = await streamFetch({
|
||||
url: '/api/core/chat/helperBot/completions',
|
||||
data: {
|
||||
chatId,
|
||||
chatItemId: chatItemDataId,
|
||||
query,
|
||||
files: chatForm.getValues('files').map((item) => ({
|
||||
type: item.type,
|
||||
key: item.key,
|
||||
// url: item.url,
|
||||
name: item.name
|
||||
})),
|
||||
metadata: {
|
||||
type: 'topAgent',
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortCtrl: abortSignal
|
||||
});
|
||||
} catch (error) {}
|
||||
setIsChatting(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<MyBox display={'flex'} flexDirection={'column'} h={'100%'} position={'relative'}>
|
||||
<ScrollData
|
||||
ScrollContainerRef={ScrollContainerRef}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
w={'100%'}
|
||||
overflow={'overlay'}
|
||||
px={[4, 0]}
|
||||
pb={3}
|
||||
>
|
||||
{chatRecords.map((item, index) => (
|
||||
<Box
|
||||
key={item._id}
|
||||
px={[3, 5]}
|
||||
w={'100%'}
|
||||
maxW={['auto', 'min(1000px, 100%)']}
|
||||
mx="auto"
|
||||
_notLast={{
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
{item.obj === ChatRoleEnum.Human && <HumanItem chat={item} />}
|
||||
{item.obj === ChatRoleEnum.AI && (
|
||||
<AIItem
|
||||
chat={item}
|
||||
isChatting={isChatting}
|
||||
isLastChild={index === chatRecords.length - 1}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</ScrollData>
|
||||
<Box
|
||||
px={[3, 5]}
|
||||
m={['0 auto 10px', '10px auto']}
|
||||
w={'100%'}
|
||||
maxW={['auto', 'min(820px, 100%)']}
|
||||
>
|
||||
<ChatInput
|
||||
TextareaDom={TextareaDom}
|
||||
chatId={chatId}
|
||||
chatForm={chatForm}
|
||||
isChatting={isChatting}
|
||||
onSendMessage={handleSendMessage}
|
||||
onStop={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
const index = (props: HelperBotProps) => {
|
||||
return (
|
||||
<HelperBotContextProvider {...props}>
|
||||
<ChatBox {...props} />
|
||||
</HelperBotContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default index;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { UserInputFileItemType } from '../ChatContainer/ChatBox/type';
|
||||
|
||||
export type onSendMessageParamsType = {
|
||||
query?: string;
|
||||
files?: UserInputFileItemType[];
|
||||
};
|
||||
export type onSendMessageFnType = (e: onSendMessageParamsType) => Promise<any>;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Box, Flex, type BoxProps, useDisclosure, HStack } from '@chakra-ui/react';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Markdown from '@/components/Markdown';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
import { useSafeState } from 'ahooks';
|
||||
import type { AppFormEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../../context';
|
||||
import { useChatTest } from '../../useChatTest';
|
||||
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { cardStyles } from '../../constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover';
|
||||
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
|
||||
import type { Form2WorkflowFnType } from '../FormComponent/type';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import HelperBot from '@/components/core/chat/HelperBot';
|
||||
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
|
||||
type Props = {
|
||||
appForm: AppFormEditFormType;
|
||||
setRenderEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
form2WorkflowFn: Form2WorkflowFnType;
|
||||
};
|
||||
const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [activeTab, setActiveTab] = useSafeState<'helper' | 'chat_debug'>('helper');
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
|
||||
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
|
||||
// agentForm2AppWorkflow dependent allDatasets
|
||||
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
|
||||
|
||||
const [workflowData, setWorkflowData] = useSafeState({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { nodes, edges } = form2WorkflowFn(appForm, t);
|
||||
setWorkflowData({ nodes, edges });
|
||||
}, [appForm, setWorkflowData, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderEdit(!datasetCiteData);
|
||||
}, [datasetCiteData, setRenderEdit]);
|
||||
|
||||
const { ChatContainer, restartChat } = useChatTest({
|
||||
...workflowData,
|
||||
chatConfig: appForm.chatConfig,
|
||||
isReady: true
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex h={'full'} gap={2}>
|
||||
<MyBox
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'full'}
|
||||
py={4}
|
||||
{...cardStyles}
|
||||
boxShadow={'3'}
|
||||
>
|
||||
<Flex px={[2, 5]} pb={2}>
|
||||
<FillRowTabs<'helper' | 'chat_debug'>
|
||||
py={1}
|
||||
list={[
|
||||
{
|
||||
label: '辅助生成',
|
||||
value: 'helper'
|
||||
},
|
||||
{
|
||||
label: t('app:chat_debug'),
|
||||
value: 'chat_debug'
|
||||
}
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={(value) => {
|
||||
setActiveTab(value);
|
||||
}}
|
||||
/>
|
||||
{!isVariableVisible && activeTab === 'chat_debug' && (
|
||||
<VariablePopover chatType={ChatTypeEnum.test} />
|
||||
)}
|
||||
<Box flex={1} />
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
restartChat();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
{activeTab === 'helper' && (
|
||||
<HelperBot type={HelperBotTypeEnum.topAgent} metadata={{}} onApply={() => {}} />
|
||||
)}
|
||||
{activeTab === 'chat_debug' && <ChatContainer />}
|
||||
</Box>
|
||||
</MyBox>
|
||||
{datasetCiteData && (
|
||||
<Box flex={'1 0 0'} w={0} maxW={'560px'} {...cardStyles} boxShadow={'3'}>
|
||||
<ChatQuoteList
|
||||
rawSearch={datasetCiteData.rawSearch}
|
||||
metadata={datasetCiteData.metadata}
|
||||
onClose={() => setCiteModalData(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const Render = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
|
||||
const { chatId } = useChatStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const chatRecordProviderParams = useMemo(
|
||||
() => ({
|
||||
chatId: chatId,
|
||||
appId: appDetail._id
|
||||
}),
|
||||
[appDetail._id, chatId]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatItemContextProvider
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
isResponseDetail={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatTest
|
||||
appForm={appForm}
|
||||
setRenderEdit={setRenderEdit}
|
||||
form2WorkflowFn={form2WorkflowFn}
|
||||
/>
|
||||
</ChatRecordContextProvider>
|
||||
</ChatItemContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import ChatTest from '../FormComponent/ChatTest';
|
||||
import ChatTest from './ChatTest';
|
||||
import AppCard from '../FormComponent/AppCard';
|
||||
import EditForm from './EditForm';
|
||||
import { type AppFormEditFormType } from '@fastgpt/global/core/app/type';
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ const EditForm = ({
|
|||
{t('common:Choose')}
|
||||
</Button>
|
||||
<Button
|
||||
mr={'-5px'}
|
||||
variant={'transparentBase'}
|
||||
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
|
||||
iconSpacing={1}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { useChatBox } from '@/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { Box, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import {
|
||||
HelperBotCompletionsParamsSchema,
|
||||
type HelperBotCompletionsParamsType
|
||||
} from '@fastgpt/global/openapi/core/chat/helperBot/api';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoHelperBotChatItem } from '@fastgpt/service/core/chat/HelperBot/chatItemSchema';
|
||||
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
|
||||
import { dispatchMap } from '@fastgpt/service/core/chat/HelperBot/dispatch/index';
|
||||
|
||||
export type completionsBody = HelperBotCompletionsParamsType;
|
||||
|
||||
async function handler(req: ApiRequestProps<completionsBody>, res: ApiResponseType<any>) {
|
||||
const { chatId, chatItemId, query, files, metadata } = HelperBotCompletionsParamsSchema.parse(
|
||||
req.body
|
||||
);
|
||||
|
||||
const { teamId, userId } = await authCert({ req, authToken: true });
|
||||
|
||||
const histories = await MongoHelperBotChatItem.find({
|
||||
userId,
|
||||
chatId
|
||||
})
|
||||
.sort({ _id: -1 })
|
||||
.limit(40)
|
||||
.lean();
|
||||
histories.reverse();
|
||||
|
||||
const workflowResponseWrite = getWorkflowResponseWrite({
|
||||
res,
|
||||
detail: true,
|
||||
streamResponse: true,
|
||||
id: chatId,
|
||||
showNodeStatus: true
|
||||
});
|
||||
|
||||
// 执行不同逻辑
|
||||
const fn = dispatchMap[metadata.type];
|
||||
const result = await fn({});
|
||||
|
||||
// Save chat
|
||||
// Push usage
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '20mb'
|
||||
},
|
||||
responseLimit: '20mb'
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type { DeleteHelperBotChatParamsType } from '@fastgpt/global/openapi/core/chat/helperBot/api';
|
||||
import { authHelperBotChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { MongoHelperBotChatItem } from '@fastgpt/service/core/chat/HelperBot/chatItemSchema';
|
||||
|
||||
export type deleteRecordQuery = DeleteHelperBotChatParamsType;
|
||||
|
||||
export type deleteRecordBody = {};
|
||||
|
||||
export type deleteRecordResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<deleteRecordBody, deleteRecordQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<deleteRecordResponse> {
|
||||
const { type, chatId, chatItemId } = req.query;
|
||||
const { chat, userId } = await authHelperBotChatCrud({
|
||||
type,
|
||||
chatId,
|
||||
req,
|
||||
authToken: true
|
||||
});
|
||||
await MongoHelperBotChatItem.deleteMany({ userId, chatId, chatItemId });
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type { GetHelperBotFilePresignParamsType } from '@fastgpt/global/openapi/core/chat/helperBot/api';
|
||||
import type { CreatePostPresignedUrlResult } from '@fastgpt/service/common/s3/type';
|
||||
import { authHelperBotChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { getS3HelperBotSource } from '../../../../../../../../packages/service/common/s3/sources/helperbot/index';
|
||||
|
||||
export type getFilePresignQuery = {};
|
||||
|
||||
export type getFilePresignBody = GetHelperBotFilePresignParamsType;
|
||||
|
||||
export type getFilePresignResponse = CreatePostPresignedUrlResult;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getFilePresignBody, getFilePresignQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<getFilePresignResponse> {
|
||||
const { type, chatId, filename } = req.body;
|
||||
|
||||
const { userId } = await authCert({
|
||||
req,
|
||||
authToken: true
|
||||
});
|
||||
|
||||
const data = await getS3HelperBotSource().createUploadFileURL({
|
||||
type,
|
||||
chatId,
|
||||
userId,
|
||||
filename
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import type { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type { GetHelperBotFilePreviewParamsType } from '@fastgpt/global/openapi/core/chat/helperBot/api';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { getS3HelperBotSource } from '@fastgpt/service/common/s3/sources/helperbot';
|
||||
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
|
||||
|
||||
async function handler(req: ApiRequestProps<GetHelperBotFilePreviewParamsType>): Promise<string> {
|
||||
const { key } = req.body;
|
||||
const { userId } = await authCert({
|
||||
req,
|
||||
authToken: true
|
||||
});
|
||||
|
||||
const { type, chatId, userId: uid, filename } = getS3HelperBotSource().parseKey(key);
|
||||
|
||||
if (userId !== uid) {
|
||||
return Promise.reject(ChatErrEnum.unAuthChat);
|
||||
}
|
||||
|
||||
return await getS3HelperBotSource().createGetFileURL({ key, external: true });
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type {
|
||||
GetHelperBotChatRecordsParamsType,
|
||||
GetHelperBotChatRecordsResponseType
|
||||
} from '@fastgpt/global/openapi/core/chat/helperBot/api';
|
||||
import { authHelperBotChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { MongoHelperBotChatItem } from '../../../../../../../../packages/service/core/chat/HelperBot/chatItemSchema';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
|
||||
export type getRecordsQuery = GetHelperBotChatRecordsParamsType;
|
||||
|
||||
export type getRecordsBody = {};
|
||||
|
||||
export type getRecordsResponse = GetHelperBotChatRecordsResponseType;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getRecordsBody, getRecordsQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<getRecordsResponse> {
|
||||
const { type, chatId } = req.query;
|
||||
const { chat, userId } = await authHelperBotChatCrud({
|
||||
type,
|
||||
chatId,
|
||||
req,
|
||||
authToken: true
|
||||
});
|
||||
|
||||
const { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
const [histories, total] = await Promise.all([
|
||||
MongoHelperBotChatItem.find({ userId, chatId }).sort({ _id: -1 }).skip(offset).limit(20).lean(),
|
||||
MongoHelperBotChatItem.countDocuments({ userId, chatId })
|
||||
]);
|
||||
histories.reverse();
|
||||
|
||||
return {
|
||||
total,
|
||||
list: histories
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -11,6 +11,9 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
|||
import { getFlatAppResponses } from '@/global/core/chat/utils';
|
||||
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import type { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
|
||||
import { MongoHelperBotChat } from '@fastgpt/service/core/chat/HelperBot/chatSchema';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
|
||||
/*
|
||||
检查chat的权限:
|
||||
|
|
@ -264,3 +267,18 @@ export const authCollectionInChat = async ({
|
|||
} catch (error) {}
|
||||
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
};
|
||||
|
||||
export const authHelperBotChatCrud = async ({
|
||||
type,
|
||||
chatId,
|
||||
...props
|
||||
}: AuthModeType & {
|
||||
type: `${HelperBotTypeEnum}`;
|
||||
chatId: string;
|
||||
}) => {
|
||||
const { userId } = await authCert(props);
|
||||
|
||||
const chat = await MongoHelperBotChat.findOne({ type, userId, chatId }).lean();
|
||||
|
||||
return { chat, userId };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||
import type { getResDataQuery } from '@/pages/api/core/chat/getResData';
|
||||
import type {
|
||||
InitChatProps,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
"**/*.d.ts",
|
||||
"../../packages/**/*.d.ts",
|
||||
"../../test/list.test.ts",
|
||||
"../../packages/service/core/workflow/dispatch/ai/agent/type.ts"
|
||||
"../../packages/service/core/workflow/dispatch/ai/agent/type.ts",
|
||||
"../../packages/service/core/workflow/dispatch/type.ts"
|
||||
],
|
||||
"exclude": ["**/*.test.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue