diff --git a/deploy/version/v4.15/docker-compose.template.yml b/deploy/version/v4.15/docker-compose.template.yml index 3c607a5f3f3f..11d5e9c1f714 100644 --- a/deploy/version/v4.15/docker-compose.template.yml +++ b/deploy/version/v4.15/docker-compose.template.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/content/self-host/config/env.en.mdx b/document/content/self-host/config/env.en.mdx index 1b6fcc743dac..7fa8c1e83f80 100644 --- a/document/content/self-host/config/env.en.mdx +++ b/document/content/self-host/config/env.en.mdx @@ -180,7 +180,6 @@ These variables are mainly validated by `packages/service/env.ts` and apply to ` | Variable | Default | Description | | -------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AGENT_ENGINE` | `default` | Agent engine. Supported values are `default` and `pi`. | -| `HELPER_BOT_MODEL` | Empty | Helper generation model. The model must be enabled in the system. | | `SKIP_FILE_TYPE_CHECK` | `false` | Whether upload file type checks are skipped. | | `WECHAT_CHANNEL_CONCURRENCY` | `1000` | WeChat channel poll worker concurrency. Minimum value is `10`. | | `PARSE_FILE_WORKERS` | `10` | Resident file parsing worker count. | diff --git a/document/content/self-host/config/env.mdx b/document/content/self-host/config/env.mdx index 6c4e86e763d7..6da66a77f4b9 100644 --- a/document/content/self-host/config/env.mdx +++ b/document/content/self-host/config/env.mdx @@ -180,7 +180,6 @@ description: projects/app、projects/code-sandbox 与 pro/admin 环境变量说 | 变量 | 默认值 | 说明 | | -------------------------------------- | --------- | ---------------------------------------------------------------------------------------------- | | `AGENT_ENGINE` | `default` | Agent 引擎,可选 `default` 或 `pi`。 | -| `HELPER_BOT_MODEL` | 空 | 辅助生成模型,需保证系统中已启用对应模型。 | | `SKIP_FILE_TYPE_CHECK` | `false` | 是否跳过上传文件类型检查。 | | `WECHAT_CHANNEL_CONCURRENCY` | `1000` | 微信渠道 poll worker 并发数,最小 `10`。 | | `PARSE_FILE_WORKERS` | `10` | 文件解析 worker 常驻线程数。 | diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index d2250d950d57..2cd755d2e3d8 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -203,8 +203,8 @@ "content/self-host/custom-models/ollama.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/custom-models/xinference.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/custom-models/xinference.mdx": "2026-04-26T21:08:47+08:00", - "content/self-host/deploy/docker.en.mdx": "2026-07-03T23:51:40+08:00", - "content/self-host/deploy/docker.mdx": "2026-07-03T23:51:40+08:00", + "content/self-host/deploy/docker.en.mdx": "2026-07-04T00:03:29+08:00", + "content/self-host/deploy/docker.mdx": "2026-07-04T00:03:29+08:00", "content/self-host/deploy/sealos.en.mdx": "2026-06-30T22:10:03+08:00", "content/self-host/deploy/sealos.mdx": "2026-06-30T22:10:03+08:00", "content/self-host/design/dataset.en.mdx": "2026-04-26T21:08:47+08:00", @@ -314,8 +314,8 @@ "content/self-host/upgrading/4-15/41506.mdx": "2026-07-01T12:13:58+08:00", "content/self-host/upgrading/4-15/41507.en.mdx": "2026-06-30T17:31:43+08:00", "content/self-host/upgrading/4-15/41507.mdx": "2026-06-30T17:31:43+08:00", - "content/self-host/upgrading/4-15/4151.en.mdx": "2026-07-03T22:44:25+08:00", - "content/self-host/upgrading/4-15/4151.mdx": "2026-07-03T22:44:25+08:00", + "content/self-host/upgrading/4-15/4151.en.mdx": "2026-07-04T00:03:29+08:00", + "content/self-host/upgrading/4-15/4151.mdx": "2026-07-04T00:03:29+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00", diff --git a/document/public/deploy/docker/v4.15/cn/docker-compose.milvus.yml b/document/public/deploy/docker/v4.15/cn/docker-compose.milvus.yml index 3f645b5c0ab5..6a40b515cc65 100644 --- a/document/public/deploy/docker/v4.15/cn/docker-compose.milvus.yml +++ b/document/public/deploy/docker/v4.15/cn/docker-compose.milvus.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/cn/docker-compose.oceanbase.yml b/document/public/deploy/docker/v4.15/cn/docker-compose.oceanbase.yml index 53ef942d4fad..c71dcbefd638 100644 --- a/document/public/deploy/docker/v4.15/cn/docker-compose.oceanbase.yml +++ b/document/public/deploy/docker/v4.15/cn/docker-compose.oceanbase.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/cn/docker-compose.opengauss.yml b/document/public/deploy/docker/v4.15/cn/docker-compose.opengauss.yml index c61ebcc14e17..ba432103fe6a 100644 --- a/document/public/deploy/docker/v4.15/cn/docker-compose.opengauss.yml +++ b/document/public/deploy/docker/v4.15/cn/docker-compose.opengauss.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/cn/docker-compose.pg.yml b/document/public/deploy/docker/v4.15/cn/docker-compose.pg.yml index f491c374317a..546f19cdea5c 100644 --- a/document/public/deploy/docker/v4.15/cn/docker-compose.pg.yml +++ b/document/public/deploy/docker/v4.15/cn/docker-compose.pg.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/cn/docker-compose.seekdb.yml b/document/public/deploy/docker/v4.15/cn/docker-compose.seekdb.yml index 6010680060e5..ca37cf3a6938 100644 --- a/document/public/deploy/docker/v4.15/cn/docker-compose.seekdb.yml +++ b/document/public/deploy/docker/v4.15/cn/docker-compose.seekdb.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/cn/docker-compose.zilliz.yml b/document/public/deploy/docker/v4.15/cn/docker-compose.zilliz.yml index b768dd904730..5be88a1cb00d 100644 --- a/document/public/deploy/docker/v4.15/cn/docker-compose.zilliz.yml +++ b/document/public/deploy/docker/v4.15/cn/docker-compose.zilliz.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/global/docker-compose.milvus.yml b/document/public/deploy/docker/v4.15/global/docker-compose.milvus.yml index fdb5696cf6a6..29dae60205d4 100644 --- a/document/public/deploy/docker/v4.15/global/docker-compose.milvus.yml +++ b/document/public/deploy/docker/v4.15/global/docker-compose.milvus.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/global/docker-compose.oceanbase.yml b/document/public/deploy/docker/v4.15/global/docker-compose.oceanbase.yml index f26044cb9392..1b12cc5008c4 100644 --- a/document/public/deploy/docker/v4.15/global/docker-compose.oceanbase.yml +++ b/document/public/deploy/docker/v4.15/global/docker-compose.oceanbase.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/global/docker-compose.opengauss.yml b/document/public/deploy/docker/v4.15/global/docker-compose.opengauss.yml index 4828cf0aaaba..fe998f84c886 100644 --- a/document/public/deploy/docker/v4.15/global/docker-compose.opengauss.yml +++ b/document/public/deploy/docker/v4.15/global/docker-compose.opengauss.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/global/docker-compose.pg.yml b/document/public/deploy/docker/v4.15/global/docker-compose.pg.yml index caeb50afc7e9..94e8a4f910ed 100644 --- a/document/public/deploy/docker/v4.15/global/docker-compose.pg.yml +++ b/document/public/deploy/docker/v4.15/global/docker-compose.pg.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/global/docker-compose.seekdb.yml b/document/public/deploy/docker/v4.15/global/docker-compose.seekdb.yml index 37cc78e51abf..a5059f33756a 100644 --- a/document/public/deploy/docker/v4.15/global/docker-compose.seekdb.yml +++ b/document/public/deploy/docker/v4.15/global/docker-compose.seekdb.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/document/public/deploy/docker/v4.15/global/docker-compose.zilliz.yml b/document/public/deploy/docker/v4.15/global/docker-compose.zilliz.yml index 6ad3fee293bf..35bac6768e6d 100644 --- a/document/public/deploy/docker/v4.15/global/docker-compose.zilliz.yml +++ b/document/public/deploy/docker/v4.15/global/docker-compose.zilliz.yml @@ -130,7 +130,6 @@ x-service-env-config: &x-service-env-config MAX_LOGIN_SESSION: ALLOWED_ORIGINS: AGENT_ENGINE: default - HELPER_BOT_MODEL: qwen-max CHAT_TITLE_MODEL: SKIP_FILE_TYPE_CHECK: false WECHAT_CHANNEL_CONCURRENCY: 1000 diff --git a/packages/global/core/ai/model.schema.ts b/packages/global/core/ai/model.schema.ts index 66fd849165f3..7b9813e0e758 100644 --- a/packages/global/core/ai/model.schema.ts +++ b/packages/global/core/ai/model.schema.ts @@ -81,7 +81,6 @@ export const LLMModelItemSchema = PriceTypeSchema.extend(BaseModelItemSchema.sha isDefaultDatasetTextModel: z.boolean().optional(), isDefaultDatasetImageModel: z.boolean().optional(), isDefaultChatTitleModel: z.boolean().optional(), - isDefaultHelperBotModel: z.boolean().optional(), /** @deprecated */ datasetProcess: z.boolean().optional(), // dataset diff --git a/packages/global/core/chat/constants.ts b/packages/global/core/chat/constants.ts index c6d5b42272a9..89d21124e86f 100644 --- a/packages/global/core/chat/constants.ts +++ b/packages/global/core/chat/constants.ts @@ -45,7 +45,8 @@ export enum ChatSourceEnum { */ export enum ChatSourceTypeEnum { app = 'app', - skillEdit = 'skillEdit' + skillEdit = 'skillEdit', + helperBot = 'helperBot' } export const ChatSourceMap = { diff --git a/packages/global/core/chat/helperBot/adaptor.ts b/packages/global/core/chat/helperBot/adaptor.ts deleted file mode 100644 index e25385fd357f..000000000000 --- a/packages/global/core/chat/helperBot/adaptor.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { ChatCompletionRequestMessageRoleEnum } from '../../ai/constants'; -import type { ChatCompletionContentPart, ChatCompletionMessageParam } from '../../ai/llm/type'; -import { ChatFileTypeEnum, ChatRoleEnum } from '../constants'; -import type { HelperBotChatItemType } from './type'; -import { isFileUrlChatFileType, simpleUserContentPart } from '../adapt'; - -export const helperChats2GPTMessages = ({ - messages -}: { - messages: HelperBotChatItemType[]; -}): 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 (isFileUrlChatFileType(item.file?.type)) { - return { - type: 'file_url', - name: item.file?.name || '', - url: item.file.url, - fileType: item.file.type, - 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 ('collectionForm' in value) { - const text = JSON.stringify( - value.collectionForm.params.inputForm.map((item) => ({ - label: item.label, - type: item.type - })) - ); - - // Concat text - const lastValue = item.value[i - 1]; - const lastResult = aiResults[aiResults.length - 1]; - if (lastValue && typeof lastResult?.content === 'string') { - lastResult.content += text; - } else { - aiResults.push({ - role: ChatCompletionRequestMessageRoleEnum.Assistant, - content: text - }); - } - } else if ('text' in value && typeof 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; -}; diff --git a/packages/global/core/chat/helperBot/constants.ts b/packages/global/core/chat/helperBot/constants.ts new file mode 100644 index 000000000000..165fa1f3cb44 --- /dev/null +++ b/packages/global/core/chat/helperBot/constants.ts @@ -0,0 +1,62 @@ +import { + AppTypeEnum, + defaultAppSelectFileConfig, + defaultQGConfig, + defaultWhisperConfig +} from '../../app/constants'; +import type { AppQGConfigType, AppWhisperConfigType } from '../../app/type'; +import type { AppFileSelectConfigType } from '../../app/type/config.schema'; + +export const HelperBotAppName = 'Top Agent'; +export const HelperBotAvatar = '/imgs/bot.svg'; + +export const helperBotFileSelectConfig: AppFileSelectConfigType = { + ...defaultAppSelectFileConfig, + maxFiles: 10, + canSelectFile: true, + canSelectImg: true, + canSelectVideo: true, + canSelectAudio: true, + customPdfParse: false, + canSelectCustomFileExtension: true, + customFileExtensionList: [] +}; + +export const helperBotQuestionGuideConfig: AppQGConfigType = { + ...defaultQGConfig, + open: false +}; + +export const helperBotWhisperConfig: AppWhisperConfigType = { + ...defaultWhisperConfig, + open: false +}; + +/** 创建 HelperBot 专用 Chat 配置,避免调用方共享并误改全局对象。 */ +export const createHelperBotChatConfig = () => { + return { + fileSelectConfig: { + ...helperBotFileSelectConfig, + customFileExtensionList: [...(helperBotFileSelectConfig.customFileExtensionList ?? [])] + }, + questionGuide: { + ...helperBotQuestionGuideConfig + }, + whisperConfig: { + ...helperBotWhisperConfig + } + }; +}; + +/** 创建 HelperBot 在通用 ChatBox 中使用的轻量 App 信息。 */ +export const createHelperBotAppConfig = () => { + return { + chatConfig: createHelperBotChatConfig(), + chatModels: [], + name: HelperBotAppName, + avatar: HelperBotAvatar, + intro: '', + type: AppTypeEnum.simple, + pluginInputs: [] + }; +}; diff --git a/packages/global/core/chat/helperBot/topAgent/type.ts b/packages/global/core/chat/helperBot/topAgent/type.ts index 0600691fa203..6006952a8deb 100644 --- a/packages/global/core/chat/helperBot/topAgent/type.ts +++ b/packages/global/core/chat/helperBot/topAgent/type.ts @@ -11,7 +11,12 @@ export const topAgentParamsSchema = z.object({ selectedDatasets: z.array(z.string()).nullish(), selectedAgentSkills: z.array(SelectedAgentSkillItemTypeSchema).nullish(), fileUpload: z.boolean().nullish(), - enableSandbox: z.boolean().nullish() + enableSandbox: z.boolean().nullish(), + modelConfig: z + .object({ + model: z.string().optional() + }) + .optional() }); export type TopAgentParamsType = z.infer; diff --git a/packages/global/core/chat/helperBot/type.ts b/packages/global/core/chat/helperBot/type.ts index 4b854170aef5..4a5e9016d29c 100644 --- a/packages/global/core/chat/helperBot/type.ts +++ b/packages/global/core/chat/helperBot/type.ts @@ -1,89 +1,7 @@ -import { ObjectIdSchema } from '../../../common/type/mongo'; import z from 'zod'; -import { ChatRoleEnum } from '../constants'; -import { UserChatItemSchema, SystemChatItemSchema } from '../type'; -import { UserInputInteractiveSchema } from '../../workflow/template/system/interactive/type'; export enum HelperBotTypeEnum { topAgent = 'topAgent' } export const HelperBotTypeEnumSchema = z.enum(Object.values(HelperBotTypeEnum)); export type HelperBotTypeEnumType = z.infer; - -export const HelperBotChatSchema = z.object({ - _id: ObjectIdSchema, - chatId: z.string(), - type: HelperBotTypeEnum, - userId: z.string(), - createTime: z.coerce.date(), - updateTime: z.coerce.date(), - metadata: z.record(z.string(), z.any()).optional() -}); -export type HelperBotChatType = z.infer; - -const AIChatContentValueSchema = z - .object({ - text: z - .object({ - content: z.string() - }) - .optional(), - reasoning: z - .object({ - content: z.string() - }) - .optional(), - hideReason: z.boolean().optional() - }) - .refine((value) => value.text || value.reasoning, { - message: 'HelperBot AI content value requires text or reasoning' - }); - -// AI schema -export const AIChatItemValueItemSchema = z.union([ - AIChatContentValueSchema, - z.object({ - collectionForm: UserInputInteractiveSchema - }), - z.object({ - planHint: z.object({ - type: z.literal('generation') - }) - }) -]); -export type AIChatItemValueItemType = z.infer; -const AIChatItemSchema = z.object({ - obj: z.literal(ChatRoleEnum.AI), - value: z.array(AIChatItemValueItemSchema) -}); -export type AIChatItemType = z.infer; - -const HelperBotChatRoleSchema = z.union([ - UserChatItemSchema, - SystemChatItemSchema, - AIChatItemSchema -]); - -export const HelperBotChatItemSchema = z.intersection( - z.object({ - _id: ObjectIdSchema, - userId: z.string(), - chatId: z.string(), - dataId: z.string(), - createTime: z.coerce.date(), - memories: z.record(z.string(), z.any()).nullish() - }), - HelperBotChatRoleSchema -); -export type HelperBotChatItemType = z.infer; - -/* 客户端 UI User Interface 展示的类型 */ -export const HelperBotChatItemSiteSchema = z.intersection( - z.object({ - _id: ObjectIdSchema, - dataId: z.string(), - createTime: z.coerce.date() - }), - HelperBotChatRoleSchema -); -export type HelperBotChatItemSiteType = z.infer; diff --git a/packages/global/core/workflow/runtime/constants.ts b/packages/global/core/workflow/runtime/constants.ts index 2a0a29ae5389..34f938c32b43 100644 --- a/packages/global/core/workflow/runtime/constants.ts +++ b/packages/global/core/workflow/runtime/constants.ts @@ -27,7 +27,6 @@ export enum SseResponseEventEnum { skillCall = 'skillCall', // skill invocation announce (when SKILL.md is loaded) // Helperbot - collectionForm = 'collectionForm', // collection form for HelperBot topAgentConfig = 'topAgentConfig' // form data for TopAgent } diff --git a/packages/global/openapi/core/chat/api.ts b/packages/global/openapi/core/chat/api.ts index 0d9190cac1d2..20630d94503b 100644 --- a/packages/global/openapi/core/chat/api.ts +++ b/packages/global/openapi/core/chat/api.ts @@ -19,12 +19,17 @@ const ChatTargetInputShape = { skillId: ObjectIdSchema.optional().meta({ example: '68ad85a7463006c963799a06', description: 'Skill Edit 调试 ID。appId、skillId 和 outLinkAuthData 必须且只能传一个。' + }), + sourceType: z.enum([ChatSourceTypeEnum.app, ChatSourceTypeEnum.helperBot]).optional().meta({ + example: ChatSourceTypeEnum.helperBot, + description: 'appId 对应的会话资源类型。缺省为 App 会话;HelperBot 会话传 helperBot。' }) }; type ChatTargetInput = { appId?: unknown; skillId?: unknown; + sourceType?: unknown; outLinkAuthData?: unknown; [key: string]: unknown; }; @@ -43,10 +48,17 @@ export type ChatTargetInputType = | { appId: string; skillId?: never; + sourceType?: ChatSourceTypeEnum.app; } | { appId?: never; skillId: string; + sourceType?: never; + } + | { + appId: string; + skillId?: never; + sourceType: ChatSourceTypeEnum.helperBot; }; export type ChatTargetResponseType = ChatTargetInputType; @@ -74,6 +86,11 @@ const getNormalizedOutLinkAuthData = (data: ChatTargetInput): OutLinkChatAuthPro }; export const ChatTargetResponseSchema = z.union([ + z.object({ + appId: ObjectIdSchema.describe('应用 ID,仅 HelperBot 会话返回'), + skillId: z.undefined().optional(), + sourceType: z.literal(ChatSourceTypeEnum.helperBot).describe('HelperBot 会话资源类型') + }), z.object({ appId: ObjectIdSchema.describe('应用 ID,仅 App 会话返回'), skillId: z.undefined().optional() @@ -107,6 +124,10 @@ export const buildChatTargetResponse = ({ return { skillId: id }; } + if (sourceType === ChatSourceTypeEnum.helperBot) { + return { appId: id, sourceType: ChatSourceTypeEnum.helperBot }; + } + const exhaustiveCheck: never = sourceType; throw new Error(`Unsupported chat source type: ${exhaustiveCheck}`); }; @@ -119,6 +140,7 @@ export const refineRequiredChatTargetInput = ( data: { appId?: unknown; skillId?: unknown; + sourceType?: unknown; outLinkAuthData?: unknown; }, ctx: z.RefinementCtx @@ -130,6 +152,7 @@ export const refineOptionalChatTargetInput = ( data: { appId?: unknown; skillId?: unknown; + sourceType?: unknown; outLinkAuthData?: unknown; }, ctx: z.RefinementCtx @@ -150,6 +173,7 @@ const refineChatAuthTargetInput = ( ) => { const hasAppTarget = !!data.appId; const hasSkillTarget = !!data.skillId; + const isHelperBotTarget = data.sourceType === ChatSourceTypeEnum.helperBot; const nestedAuthData = getNestedOutLinkAuthData(data); const shareId = nestedAuthData.shareId; const outLinkUid = nestedAuthData.outLinkUid; @@ -165,6 +189,27 @@ const refineChatAuthTargetInput = ( }); } + if ([hasAppTarget, hasSkillTarget, hasCompleteShareAuth].filter(Boolean).length > 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'appId, skillId and share auth cannot be provided at the same time' + }); + } + + if (isHelperBotTarget && !hasAppTarget) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'sourceType=helperBot requires appId' + }); + } + + if (isHelperBotTarget && hasSkillTarget) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'sourceType=helperBot cannot be used with skillId' + }); + } + if (hasShareAuth && !hasCompleteShareAuth) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -201,12 +246,16 @@ const refineChatAuthTargetInput = ( * raw schema,避免文档暴露内部 `sourceType/sourceId`。 */ export const transformChatTargetInput = (data: T) => { - const { appId, skillId, ...rest } = data; + const { appId, skillId, sourceType: rawSourceType, ...rest } = data; const sourceId = (appId || skillId) as string; return { ...rest, - sourceType: appId ? ChatSourceTypeEnum.app : ChatSourceTypeEnum.skillEdit, + sourceType: (() => { + if (skillId) return ChatSourceTypeEnum.skillEdit; + if (rawSourceType === ChatSourceTypeEnum.helperBot) return ChatSourceTypeEnum.helperBot; + return ChatSourceTypeEnum.app; + })(), sourceId }; }; @@ -218,7 +267,7 @@ export const transformChatTargetInput = (data: T) => * 外链上下文统一收敛到 `outLinkAuthData`,再由 `authChatTargetCrud` 解析真实 sourceId。 */ export const transformChatAuthTargetInput = (data: T) => { - const { appId, skillId, ...rest } = data; + const { appId, skillId, sourceType: rawSourceType, ...rest } = data; delete rest.outLinkAuthData; const sourceId = (appId || skillId) as string | undefined; const normalizedOutLinkAuthData = getNormalizedOutLinkAuthData(data); @@ -238,7 +287,11 @@ export const transformChatAuthTargetInput = (data: T) return { ...rest, ...(normalizedOutLinkAuthData ? { outLinkAuthData: normalizedOutLinkAuthData } : {}), - sourceType: appId ? ChatSourceTypeEnum.app : ChatSourceTypeEnum.skillEdit, + sourceType: (() => { + if (skillId) return ChatSourceTypeEnum.skillEdit; + if (rawSourceType === ChatSourceTypeEnum.helperBot) return ChatSourceTypeEnum.helperBot; + return ChatSourceTypeEnum.app; + })(), sourceId: sourceId! }; }; @@ -250,7 +303,7 @@ export const transformChatAuthTargetInput = (data: T) * `sourceType/sourceId`。 */ export const transformOptionalChatTargetInput = (data: T) => { - const { appId, skillId, ...rest } = data; + const { appId, skillId, sourceType: rawSourceType, ...rest } = data; delete rest.outLinkAuthData; const sourceId = (appId || skillId) as string | undefined; const normalizedOutLinkAuthData = getNormalizedOutLinkAuthData(data); @@ -272,7 +325,11 @@ export const transformOptionalChatTargetInput = (data ...(normalizedOutLinkAuthData ? { outLinkAuthData: normalizedOutLinkAuthData } : {}), ...(appId || skillId ? { - sourceType: appId ? ChatSourceTypeEnum.app : ChatSourceTypeEnum.skillEdit, + sourceType: (() => { + if (skillId) return ChatSourceTypeEnum.skillEdit; + if (rawSourceType === ChatSourceTypeEnum.helperBot) return ChatSourceTypeEnum.helperBot; + return ChatSourceTypeEnum.app; + })(), sourceId: sourceId! } : { diff --git a/packages/global/openapi/core/chat/helperBot/api.ts b/packages/global/openapi/core/chat/helperBot/api.ts index 6129e8972492..a085771ec7d9 100644 --- a/packages/global/openapi/core/chat/helperBot/api.ts +++ b/packages/global/openapi/core/chat/helperBot/api.ts @@ -1,65 +1,40 @@ import { z } from 'zod'; -import { PaginationResponseSchema, PaginationSchema } from '../../../api'; +import { ObjectIdSchema } from '../../../../common/type/mongo'; import { ChatFileTypeEnum } from '../../../../core/chat/constants'; -import { - HelperBotChatItemSiteSchema, - HelperBotTypeEnum, - HelperBotTypeEnumSchema -} from '../../../../core/chat/helperBot/type'; +import { HelperBotTypeEnum } from '../../../../core/chat/helperBot/type'; import { topAgentParamsSchema } from '../../../../core/chat/helperBot/topAgent/type'; +import { ChatCompletionMessageParamSchema } from '../../../../core/ai/llm/type'; +import { WorkflowInteractiveResponseTypeSchema } from '../../../../core/workflow/template/system/interactive/type'; -export const HelperBotCompletionsParamsSchema = z.object({ - chatId: z.string(), - chatItemId: z.string(), - query: z.string(), - files: z.array( - z.object({ - type: z.enum(ChatFileTypeEnum), - key: z.string(), - url: z.string().optional(), - name: z.string() - }) - ), - metadata: z.object({ - type: z.literal(HelperBotTypeEnum.topAgent), - data: topAgentParamsSchema - }) -}); -export type HelperBotCompletionsParamsType = z.infer; - -// 分页获取记录 -export const GetHelperBotChatRecordsParamsSchema = PaginationSchema.extend({ - type: HelperBotTypeEnumSchema, - chatId: z.string() -}); -export type GetHelperBotChatRecordsParamsType = z.infer; -export const GetHelperBotChatRecordsResponseSchema = PaginationResponseSchema( - HelperBotChatItemSiteSchema -); -export type GetHelperBotChatRecordsResponseType = z.infer< - typeof GetHelperBotChatRecordsResponseSchema ->; - -// 删除单组对话 -export const DeleteHelperBotChatParamsSchema = z.object({ - type: HelperBotTypeEnumSchema, - chatId: z.string(), - chatItemId: z.string() +export const HelperBotChatFileSchema = z.object({ + type: z.enum(ChatFileTypeEnum), + key: z.string(), + url: z.string().optional(), + name: z.string() }); -export type DeleteHelperBotChatParamsType = z.infer; +export type HelperBotChatFileType = z.infer; -// 获取文件上传签名 -export const GetHelperBotFilePresignParamsSchema = z.object({ - type: HelperBotTypeEnumSchema, - chatId: z.string(), - filename: z.string() -}); -export type GetHelperBotFilePresignParamsType = z.infer; +export const HelperBotCompletionsParamsSchema = z + .object({ + chatId: z.string(), + responseChatItemId: z.string(), + appId: ObjectIdSchema, + messages: z.array(ChatCompletionMessageParamSchema), + interactive: WorkflowInteractiveResponseTypeSchema.optional(), + metadata: z.object({ + type: z.literal(HelperBotTypeEnum.topAgent), + data: topAgentParamsSchema + }) + }) + .superRefine(({ messages }, ctx) => { + const lastUserMessage = messages.findLast((message) => message.role === 'user'); -// 获取文件预览链接 -export const GetHelperBotFilePreviewParamsSchema = z.object({ - key: z.string().min(1), - mode: z.enum(['proxy', 'presigned']).optional() -}); -export type GetHelperBotFilePreviewParamsType = z.infer; -export const GetHelperBotFilePreviewResponseSchema = z.string(); + if (!lastUserMessage?.dataId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['messages'], + message: 'HelperBot messages requires a user message with dataId' + }); + } + }); +export type HelperBotCompletionsParamsType = z.infer; diff --git a/packages/global/openapi/core/chat/helperBot/index.ts b/packages/global/openapi/core/chat/helperBot/index.ts index 36f97e711844..8146527a04d9 100644 --- a/packages/global/openapi/core/chat/helperBot/index.ts +++ b/packages/global/openapi/core/chat/helperBot/index.ts @@ -1,16 +1,7 @@ import z from 'zod'; import type { OpenAPIPath } from '../../../type'; import { DevApiTagsMap } from '../../../tag'; -import { - HelperBotCompletionsParamsSchema, - DeleteHelperBotChatParamsSchema, - GetHelperBotFilePresignParamsSchema, - GetHelperBotFilePreviewParamsSchema, - GetHelperBotFilePreviewResponseSchema, - GetHelperBotChatRecordsParamsSchema, - GetHelperBotChatRecordsResponseSchema -} from './api'; -import { CreatePostPresignedUrlResponseSchema } from '../../../../common/file/s3/type'; +import { HelperBotCompletionsParamsSchema } from './api'; export const HelperBotPath: OpenAPIPath = { '/proApi/core/chat/helperBot/completions': { @@ -36,97 +27,5 @@ export const HelperBotPath: OpenAPIPath = { } } } - }, - '/core/chat/helperBot/getRecords': { - get: { - summary: '分页获取记录', - description: '分页获取记录', - tags: [DevApiTagsMap.helperBot], - requestParams: { - query: GetHelperBotChatRecordsParamsSchema - }, - responses: { - 200: { - description: '成功返回记录列表', - content: { - 'application/json': { - schema: GetHelperBotChatRecordsResponseSchema - } - } - } - } - } - }, - '/core/chat/helperBot/deleteRecord': { - delete: { - summary: '删除单组对话', - description: '删除单组对话', - tags: [DevApiTagsMap.helperBot], - requestBody: { - content: { - 'application/json': { - schema: DeleteHelperBotChatParamsSchema - } - } - }, - responses: { - 200: { - description: '成功删除记录', - content: { - 'application/json': { - schema: z.any() - } - } - } - } - } - }, - '/core/chat/helperBot/getFilePresign': { - post: { - summary: '获取辅助助手文件上传签名', - description: '获取辅助助手文件上传签名', - tags: [DevApiTagsMap.helperBot], - requestBody: { - content: { - 'application/json': { - schema: GetHelperBotFilePresignParamsSchema - } - } - }, - responses: { - 200: { - description: '成功返回文件上传签名', - content: { - 'application/json': { - schema: CreatePostPresignedUrlResponseSchema - } - } - } - } - } - }, - '/core/chat/helperBot/getFilePreviewUrl': { - post: { - summary: '获取辅助助手文件预览链接', - description: '获取辅助助手文件预览链接', - tags: [DevApiTagsMap.helperBot], - requestBody: { - content: { - 'application/json': { - schema: GetHelperBotFilePreviewParamsSchema - } - } - }, - responses: { - 200: { - description: '成功返回文件预览链接', - content: { - 'application/json': { - schema: GetHelperBotFilePreviewResponseSchema - } - } - } - } - } } }; diff --git a/packages/global/test/core/chat/helperBot/adaptor.test.ts b/packages/global/test/core/chat/helperBot/adaptor.test.ts deleted file mode 100644 index 3f386365f825..000000000000 --- a/packages/global/test/core/chat/helperBot/adaptor.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { helperChats2GPTMessages } from '@fastgpt/global/core/chat/helperBot/adaptor'; -import { ChatRoleEnum, ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; -import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; -import type { HelperBotChatItemType } from '@fastgpt/global/core/chat/helperBot/type'; - -describe('helperChats2GPTMessages', () => { - it('should convert system message', () => { - const messages = [ - { - obj: ChatRoleEnum.System, - value: [{ text: { content: 'You are a helpful assistant' } }] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.System); - expect(result[0].content).toBe('You are a helpful assistant'); - }); - - it('should skip system message with empty content', () => { - const messages = [ - { - obj: ChatRoleEnum.System, - value: [{ text: { content: '' } }] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(0); - }); - - it('should convert human message with text', () => { - const messages = [ - { - obj: ChatRoleEnum.Human, - value: [{ text: { content: 'Hello' } }] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.User); - expect(result[0].content).toBe('Hello'); - }); - - it('should convert human message with image', () => { - const messages = [ - { - obj: ChatRoleEnum.Human, - value: [ - { - file: { - type: ChatFileTypeEnum.image, - name: 'test.png', - url: 'http://example.com/test.png', - key: 'test-key' - } - } - ] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.User); - expect(Array.isArray(result[0].content)).toBe(true); - const content = result[0].content as any[]; - expect(content[0].type).toBe('image_url'); - expect(content[0].image_url.url).toBe('http://example.com/test.png'); - }); - - it('should convert human message with file', () => { - const messages = [ - { - obj: ChatRoleEnum.Human, - value: [ - { - file: { - type: ChatFileTypeEnum.file, - name: 'document.pdf', - url: 'http://example.com/document.pdf', - key: 'doc-key' - } - } - ] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - const content = result[0].content as any[]; - expect(content[0].type).toBe('file_url'); - expect(content[0].name).toBe('document.pdf'); - }); - - it('should convert AI message with text', () => { - const messages = [ - { - obj: ChatRoleEnum.AI, - value: [{ text: { content: 'Hello, how can I help?' } }] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant); - expect(result[0].content).toBe('Hello, how can I help?'); - }); - - it('should convert merged AI reasoning and text value with visible text', () => { - const messages = [ - { - obj: ChatRoleEnum.AI, - value: [ - { - reasoning: { content: 'Hidden thinking' }, - hideReason: true, - text: { content: 'Visible answer' } - } - ] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant); - expect(result[0].content).toBe('Visible answer'); - }); - - it('should concat multiple AI text values', () => { - const messages = [ - { - obj: ChatRoleEnum.AI, - value: [{ text: { content: 'Part 1' } }, { text: { content: ' Part 2' } }] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].content).toBe('Part 1 Part 2'); - }); - - it('should add empty assistant message when AI has no content', () => { - const messages = [ - { - obj: ChatRoleEnum.AI, - value: [] - } - ] as unknown as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant); - expect(result[0].content).toBe(''); - }); - - it('should handle mixed message types', () => { - const messages = [ - { - obj: ChatRoleEnum.System, - value: [{ text: { content: 'System prompt' } }] - }, - { - obj: ChatRoleEnum.Human, - value: [{ text: { content: 'User question' } }] - }, - { - obj: ChatRoleEnum.AI, - value: [{ text: { content: 'AI response' } }] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(3); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.System); - expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.User); - expect(result[2].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant); - }); - - it('should handle collectionForm in AI message', () => { - const messages = [ - { - obj: ChatRoleEnum.AI, - value: [ - { - collectionForm: { - type: 'userInput', - params: { - description: 'Please fill the form', - inputForm: [ - { label: 'Name', type: 'input', key: 'name', required: true, value: '' } - ] - } - } - } - ] - } - ] as HelperBotChatItemType[]; - - const result = helperChats2GPTMessages({ messages }); - - expect(result).toHaveLength(1); - expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant); - expect(typeof result[0].content).toBe('string'); - expect(result[0].content).toContain('Name'); - }); -}); diff --git a/packages/global/test/core/chat/helperBot/type.test.ts b/packages/global/test/core/chat/helperBot/type.test.ts index 693ea9233eb1..443704d329ca 100644 --- a/packages/global/test/core/chat/helperBot/type.test.ts +++ b/packages/global/test/core/chat/helperBot/type.test.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from 'vitest'; import { HelperBotTypeEnum, - HelperBotTypeEnumSchema, - HelperBotChatSchema, - HelperBotChatItemSchema, - HelperBotChatItemSiteSchema, - AIChatItemValueItemSchema + HelperBotTypeEnumSchema } from '@fastgpt/global/core/chat/helperBot/type'; describe('HelperBotTypeEnum', () => { @@ -31,49 +27,3 @@ describe('HelperBotTypeEnumSchema', () => { expect(result.success).toBe(false); }); }); - -describe('AIChatItemValueItemSchema', () => { - it('should validate text content', () => { - const result = AIChatItemValueItemSchema.safeParse({ - text: { content: 'Hello' } - }); - expect(result.success).toBe(true); - }); - - it('should validate reasoning content', () => { - const result = AIChatItemValueItemSchema.safeParse({ - reasoning: { content: 'Let me think...' } - }); - expect(result.success).toBe(true); - }); - - it('should validate merged text and hidden reasoning content', () => { - const result = AIChatItemValueItemSchema.safeParse({ - reasoning: { content: 'Hidden thinking' }, - hideReason: true, - text: { content: 'Visible answer' } - }); - expect(result.success).toBe(true); - }); - - it('should reject content value without text or reasoning', () => { - const result = AIChatItemValueItemSchema.safeParse({ - hideReason: true - }); - expect(result.success).toBe(false); - }); - - it('should validate planHint', () => { - const result = AIChatItemValueItemSchema.safeParse({ - planHint: { type: 'generation' } - }); - expect(result.success).toBe(true); - }); - - it('should reject invalid planHint type', () => { - const result = AIChatItemValueItemSchema.safeParse({ - planHint: { type: 'invalid' } - }); - expect(result.success).toBe(false); - }); -}); diff --git a/packages/global/test/openapi/core/chat/targetSchema.test.ts b/packages/global/test/openapi/core/chat/targetSchema.test.ts index 10f339e06ab6..fee3fe2e9fd4 100644 --- a/packages/global/test/openapi/core/chat/targetSchema.test.ts +++ b/packages/global/test/openapi/core/chat/targetSchema.test.ts @@ -59,6 +59,21 @@ describe('openapi/core/chat target schema', () => { expect('skillId' in result).toBe(false); }); + it('transforms raw appId with helperBot sourceType to internal helperBot source', () => { + const result = StopV2ChatSchema.parse({ + appId, + sourceType: ChatSourceTypeEnum.helperBot, + chatId: 'chat-1' + }); + + expect(result).toMatchObject({ + sourceType: ChatSourceTypeEnum.helperBot, + sourceId: appId, + chatId: 'chat-1' + }); + expect('appId' in result).toBe(false); + }); + it('transforms question guide skillId to internal skillEdit source with debug model config', () => { const result = CreateQuestionGuideV2BodySchema.parse({ skillId, @@ -81,7 +96,7 @@ describe('openapi/core/chat target schema', () => { expect('skillId' in result).toBe(false); }); - it('rejects required target when appId and skillId are both provided', () => { + it('rejects required target when multiple explicit targets are provided', () => { const payload = { appId, skillId, @@ -89,6 +104,13 @@ describe('openapi/core/chat target schema', () => { }; expect(() => MarkChatReadBodySchema.parse(payload)).toThrow(); + expect(() => + StopV2ChatSchema.parse({ + skillId, + sourceType: ChatSourceTypeEnum.helperBot, + chatId: 'chat-1' + }) + ).toThrow(); expect(() => GetPaginationRecordsBodySchema.parse({ ...payload, diff --git a/packages/service/common/s3/contracts/type.ts b/packages/service/common/s3/contracts/type.ts index 827499dbfb3b..b6eb87981195 100644 --- a/packages/service/common/s3/contracts/type.ts +++ b/packages/service/common/s3/contracts/type.ts @@ -26,14 +26,7 @@ export const UploadConstraintsInputSchema = z.object({ }); export type UploadConstraintsInput = z.infer; -export const S3SourcesSchema = z.enum([ - 'avatar', - 'chat', - 'dataset', - 'temp', - 'rawText', - 'helperBot' -]); +export const S3SourcesSchema = z.enum(['avatar', 'chat', 'dataset', 'temp', 'rawText']); export const S3Sources = S3SourcesSchema.enum; export type S3SourceType = z.infer; diff --git a/packages/service/common/s3/sources/helperbot/global.d.ts b/packages/service/common/s3/sources/helperbot/global.d.ts deleted file mode 100644 index a07233b71b12..000000000000 --- a/packages/service/common/s3/sources/helperbot/global.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { S3HelperBotSource } from './index'; - -declare global { - var helperBotBucket: S3HelperBotSource; -} - -export {}; diff --git a/packages/service/common/s3/sources/helperbot/index.ts b/packages/service/common/s3/sources/helperbot/index.ts deleted file mode 100644 index a5f02067769c..000000000000 --- a/packages/service/common/s3/sources/helperbot/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools'; -import { S3PrivateBucket } from '../../buckets/private'; -import { S3Sources } from '../../contracts/type'; -import { - type CheckHelperBotFileKeys, - type DelChatFileByPrefixParams, - DelChatFileByPrefixSchema, - HelperBotFileUploadSchema -} from './type'; -import { differenceInHours } from 'date-fns'; -import { S3Buckets } from '../../config/constants'; -import path from 'path'; -import { getFileS3Key } from '../../utils'; -import { parseHelperBotFileS3Key } from './key'; - -export class S3HelperBotSource extends S3PrivateBucket { - private static instance: S3HelperBotSource; - - constructor() { - super(); - } - - 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) { - return parseHelperBotFileS3Key(key); - } - - async createGetFileURL(params: { - key: string; - expiredHours?: number; - external: boolean; - mode?: 'proxy' | 'presigned'; - }) { - const { key, expiredHours = 1, external = false, mode } = params; // 默认一个小时 - - if (external) { - return await this.createExternalUrl({ key, expiredHours, mode }); - } - return await this.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.createPresignedPutUrl( - { rawKey: fileKey, filename }, - { expiredHours: expiredTime ? differenceInHours(expiredTime, new Date()) : 1 } - ); - } - - deleteFilesByPrefix(params: DelChatFileByPrefixParams) { - const { type, chatId, userId } = DelChatFileByPrefixSchema.parse(params); - - const prefix = [S3Sources.helperBot, type, userId, chatId].filter(Boolean).join('/'); - return this.addDeleteJob({ prefix }); - } - - deleteFileByKey(key: string) { - return this.addDeleteJob({ key }); - } -} - -export function getS3HelperBotSource() { - if (global.helperBotBucket) { - return global.helperBotBucket; - } - global.helperBotBucket = new S3HelperBotSource(); - return global.helperBotBucket; -} diff --git a/packages/service/common/s3/sources/helperbot/key.ts b/packages/service/common/s3/sources/helperbot/key.ts deleted file mode 100644 index 3a85ae19ae64..000000000000 --- a/packages/service/common/s3/sources/helperbot/key.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { HelperBotTypeEnumType } from '@fastgpt/global/core/chat/helperBot/type'; -import { HelperBotTypeEnumSchema } from '@fastgpt/global/core/chat/helperBot/type'; -import { isS3ObjectKey } from '../../utils'; - -/** - * 解析 HelperBot 文件 key。 - * - * HelperBot 文件 key 的第一段是固定 source,后续才是 type/user/chat 维度。 - */ -export function parseHelperBotFileS3Key(key: string): { - type: HelperBotTypeEnumType; - userId: string; - chatId: string; - filename: string; -} | null { - if (!isS3ObjectKey(key, 'helperBot')) return null; - - const [, type, userId, chatId, ...filenameParts] = key.split('/'); - const filename = filenameParts.join('/'); - const parsedType = HelperBotTypeEnumSchema.safeParse(type); - - if (!parsedType.success || !userId || !chatId || !filename) return null; - - return { - type: parsedType.data, - userId, - chatId, - filename - }; -} - -/** - * 判断 HelperBot 文件 key 是否属于当前用户。 - */ -export function isAuthorizedHelperBotFileS3Key({ key, userId }: { key: string; userId: string }) { - const parsedKey = parseHelperBotFileS3Key(key); - - return !!parsedKey && String(parsedKey.userId) === String(userId); -} diff --git a/packages/service/common/s3/sources/helperbot/type.ts b/packages/service/common/s3/sources/helperbot/type.ts deleted file mode 100644 index b9bda1c85e82..000000000000 --- a/packages/service/common/s3/sources/helperbot/type.ts +++ /dev/null @@ -1,18 +0,0 @@ -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.coerce.date().optional() -}); -export type CheckHelperBotFileKeys = z.infer; - -export const DelChatFileByPrefixSchema = z.object({ - type: HelperBotTypeEnumSchema, - chatId: z.string().nonempty().optional(), - userId: z.string().nonempty().optional() -}); -export type DelChatFileByPrefixParams = z.infer; diff --git a/packages/service/common/s3/utils.ts b/packages/service/common/s3/utils.ts index e66da244c1fd..de1f44d44f21 100644 --- a/packages/service/common/s3/utils.ts +++ b/packages/service/common/s3/utils.ts @@ -10,7 +10,6 @@ import { S3PublicBucket } from './buckets/public'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import path from 'node:path'; import type { ParsedFileContentS3KeyParams } from './sources/dataset/type'; -import type { HelperBotTypeEnumType } from '@fastgpt/global/core/chat/helperBot/type'; // S3文件名最大长度配置 export const S3_FILENAME_MAX_LENGTH = 50; @@ -234,25 +233,6 @@ 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; diff --git a/packages/service/core/ai/config/utils.ts b/packages/service/core/ai/config/utils.ts index 02a699017eb1..96960e91fd99 100644 --- a/packages/service/core/ai/config/utils.ts +++ b/packages/service/core/ai/config/utils.ts @@ -23,7 +23,6 @@ import { refreshVersionKey } from '../../../common/cache'; import { SystemCacheKeyEnum } from '../../../common/cache/type'; import { getLogger, LogCategories } from '../../../common/logger'; import { getRuntimeResolvedPriceTiers } from '@fastgpt/global/core/ai/pricing'; -import { serviceEnv } from '../../../env'; export const loadSystemModels = async (init = false, language = 'en') => { if (!init && global.systemModelList) return; @@ -80,9 +79,6 @@ export const loadSystemModels = async (init = false, language = 'en') => { if (model.isDefaultChatTitleModel) { _systemDefaultModel.chatTitleLLM = model; } - if (model.model === serviceEnv.HELPER_BOT_MODEL) { - _systemDefaultModel.helperBotLLM = model; - } } else if (model.type === ModelTypeEnum.embedding) { _embeddingModelMap.set(model.model, model); _embeddingModelMap.set(model.name, model); @@ -191,10 +187,6 @@ export const loadSystemModels = async (init = false, language = 'en') => { (item) => item.vision ); } - if (!_systemDefaultModel.helperBotLLM) { - // HELPER_BOT_MODEL 未配置或未命中已启用模型时,回退到系统默认 LLM。 - _systemDefaultModel.helperBotLLM = _systemDefaultModel.llm; - } if (!_systemDefaultModel.embedding) { _systemDefaultModel.embedding = Array.from(_embeddingModelMap.values())[0]; } diff --git a/packages/service/core/ai/model.ts b/packages/service/core/ai/model.ts index b625bb4045ac..2928e7605d34 100644 --- a/packages/service/core/ai/model.ts +++ b/packages/service/core/ai/model.ts @@ -33,9 +33,6 @@ export const getVlmModel = (model?: string) => { export const getDefaultChatTitleModel = () => global?.systemDefaultModel.chatTitleLLM; -export const getDefaultHelperBotModel = (): LLMModelItemType => - global?.systemDefaultModel.helperBotLLM || getDefaultLLMModel(); - export const getDefaultEmbeddingModel = () => global?.systemDefaultModel.embedding!; export const getEmbeddingModel = (model?: string | EmbeddingModelItemType) => { if (!model) return getDefaultEmbeddingModel(); diff --git a/packages/service/core/ai/sandbox/infrastructure/instance/repository.ts b/packages/service/core/ai/sandbox/infrastructure/instance/repository.ts index b943175204c0..7929883ba3e6 100644 --- a/packages/service/core/ai/sandbox/infrastructure/instance/repository.ts +++ b/packages/service/core/ai/sandbox/infrastructure/instance/repository.ts @@ -93,6 +93,9 @@ const buildSandboxResourcesToArchiveQuery = (params: { const buildSandboxResourceSourceQuery = ({ sourceType, sourceId }: SandboxSourceParams) => { if (sourceType === ChatSourceTypeEnum.app) return { sourceType, sourceId }; if (sourceType === ChatSourceTypeEnum.skillEdit) return { sourceType, sourceId }; + if (sourceType === ChatSourceTypeEnum.helperBot) { + throw new Error('HelperBot source does not support sandbox resources'); + } const exhaustiveCheck: never = sourceType; throw new Error(`Unsupported sandbox source type: ${exhaustiveCheck}`); diff --git a/packages/service/core/ai/sandbox/interface/runtime.ts b/packages/service/core/ai/sandbox/interface/runtime.ts index 7ecb6b08a136..268f1d24cd5e 100644 --- a/packages/service/core/ai/sandbox/interface/runtime.ts +++ b/packages/service/core/ai/sandbox/interface/runtime.ts @@ -96,6 +96,10 @@ export function buildSandboxClientQueryFromChatSource({ }; } + if (sourceType === ChatSourceTypeEnum.helperBot) { + throw new Error('HelperBot source does not support sandbox runtime'); + } + const exhaustiveCheck: never = sourceType; throw new Error(`Unsupported chat source type: ${exhaustiveCheck}`); } diff --git a/packages/service/core/ai/sandbox/utils/id.ts b/packages/service/core/ai/sandbox/utils/id.ts index 10973d24a103..aac964e58cc6 100644 --- a/packages/service/core/ai/sandbox/utils/id.ts +++ b/packages/service/core/ai/sandbox/utils/id.ts @@ -27,6 +27,10 @@ export const getRunningSandboxId = ({ return getEditDebugSandboxId(sourceId); } + if (sourceType === ChatSourceTypeEnum.helperBot) { + throw new Error('HelperBot source does not support sandbox id generation'); + } + const exhaustiveCheck: never = sourceType; throw new Error(`Unsupported chat source type: ${exhaustiveCheck}`); }; diff --git a/packages/service/core/ai/skill/debugChat/handler.ts b/packages/service/core/ai/skill/debugChat/handler.ts index 88ffbb2e2b5a..2acec4f817ff 100644 --- a/packages/service/core/ai/skill/debugChat/handler.ts +++ b/packages/service/core/ai/skill/debugChat/handler.ts @@ -34,6 +34,7 @@ import { getDefaultLLMModel } from '../../model'; import { getRunningSkillEditSandbox } from '../../sandbox/interface/skillEdit'; import { dispatchWorkFlow } from '../../../workflow/dispatch'; import { WORKFLOW_MAX_RUN_TIMES } from '../../../workflow/constants'; +import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config.schema'; import { getChatItems } from '../../../chat/controller'; import { failChatRound, @@ -51,6 +52,16 @@ import { buildDebugRuntimeNodes } from './runtime'; import type { AgentSandboxPrepareAction } from '../../../workflow/dispatch/ai/agent/sub/sandbox'; const logger = getLogger(LogCategories.MODULE.AGENT_SKILLS); +const skillDebugFileSelectConfig: AppFileSelectConfigType = { + maxFiles: 10, + canSelectFile: true, + canSelectImg: true, + customPdfParse: false, + canSelectVideo: true, + canSelectAudio: true, + canSelectCustomFileExtension: false, + customFileExtensionList: [] +}; /** * 处理 Skill 调试对话的共享主流程。 @@ -206,7 +217,9 @@ export async function handleSkillDebugChat( variables: {}, query: removeEmptyUserInput(userQuestion.value), lastInteractive: interactive, - chatConfig: {}, + chatConfig: { + fileSelectConfig: skillDebugFileSelectConfig + }, histories: newHistories, stream: true, maxRunTimes: WORKFLOW_MAX_RUN_TIMES, diff --git a/packages/service/core/ai/skill/debugChat/runtime.ts b/packages/service/core/ai/skill/debugChat/runtime.ts index 9bbe910fbbd3..1b980752dbc9 100644 --- a/packages/service/core/ai/skill/debugChat/runtime.ts +++ b/packages/service/core/ai/skill/debugChat/runtime.ts @@ -101,6 +101,13 @@ export function buildDebugRuntimeNodes( label: 'System Prompt', value: systemPrompt }, + { + key: NodeInputKeyEnum.aiChatVision, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.boolean, + label: '', + value: true + }, { key: NodeInputKeyEnum.editSkillId, renderTypeList: [FlowNodeInputTypeEnum.hidden], diff --git a/packages/service/core/ai/type.d.ts b/packages/service/core/ai/type.d.ts index d3d8fbd762bd..127e1d7116ef 100644 --- a/packages/service/core/ai/type.d.ts +++ b/packages/service/core/ai/type.d.ts @@ -30,7 +30,6 @@ export type SystemDefaultModelType = { datasetTextLLM?: LLMModelItemType; datasetImageLLM?: LLMModelItemType; chatTitleLLM?: LLMModelItemType; - helperBotLLM?: LLMModelItemType; [ModelTypeEnum.embedding]?: EmbeddingModelItemType; [ModelTypeEnum.tts]?: TTSModelType; diff --git a/packages/service/core/chat/HelperBot/chatItemSchema.ts b/packages/service/core/chat/HelperBot/chatItemSchema.ts deleted file mode 100644 index c7d749df3413..000000000000 --- a/packages/service/core/chat/HelperBot/chatItemSchema.ts +++ /dev/null @@ -1,42 +0,0 @@ -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( - helperBotChatItemCollectionName, - HelperBotChatItemSchema -); diff --git a/packages/service/core/chat/HelperBot/chatSchema.ts b/packages/service/core/chat/HelperBot/chatSchema.ts deleted file mode 100644 index 25fafc78fb49..000000000000 --- a/packages/service/core/chat/HelperBot/chatSchema.ts +++ /dev/null @@ -1,37 +0,0 @@ -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() - }, - metadata: Object -}); - -HelperBotChatSchema.index({ type: 1, userId: 1, chatId: 1 }, { unique: true }); - -export const MongoHelperBotChat = getMongoModel( - helperBotChatCollectionName, - HelperBotChatSchema -); diff --git a/packages/service/core/chat/HelperBot/constants.ts b/packages/service/core/chat/HelperBot/constants.ts deleted file mode 100644 index a0d4a00d0e78..000000000000 --- a/packages/service/core/chat/HelperBot/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const helperBotChatCollectionName = 'helper_bot_chats'; -export const helperBotChatItemCollectionName = 'helper_bot_chat_items'; diff --git a/packages/service/core/chat/HelperBot/utils.ts b/packages/service/core/chat/HelperBot/utils.ts index 16c1a23e1ba4..ba1e9ca56533 100644 --- a/packages/service/core/chat/HelperBot/utils.ts +++ b/packages/service/core/chat/HelperBot/utils.ts @@ -1,39 +1,297 @@ import type { + AIChatItemType, AIChatItemValueItemType, - HelperBotTypeEnum -} from '@fastgpt/global/core/chat/helperBot/type'; -import type { HelperBotCompletionsParamsType } from '@fastgpt/global/openapi/core/chat/helperBot/api'; -import { MongoHelperBotChat } from './chatSchema'; -import type { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; -import { MongoHelperBotChatItem } from './chatItemSchema'; -import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; + UserChatItemType, + UserChatItemValueItemType +} from '@fastgpt/global/core/chat/type'; +import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/llm/type'; +import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; +import { + ChatFileTypeEnum, + ChatGenerateStatusEnum, + ChatRoleEnum, + ChatSourceEnum, + ChatSourceTypeEnum +} from '@fastgpt/global/core/chat/constants'; +import type { HelperBotChatFileType } from '@fastgpt/global/openapi/core/chat/helperBot/api'; import { mongoSessionRun } from '../../../common/mongo/sessionRun'; +import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { MongoChat } from '../chatSchema'; +import { MongoChatItem } from '../chatItemSchema'; +import { buildChatSourceQuery } from '../source'; +import { finalizeChatRound, persistChatFiles } from '../saveChat'; +import { createChatFilePreviewUrlGetter } from '../../../common/s3/sources/chat'; +import { isAuthorizedChatFileS3Key } from '../../../common/s3/sources/chat/key'; +const parseUserInputValue = (query: string) => { + try { + const result = JSON.parse(query); + if (result && typeof result === 'object' && !Array.isArray(result)) { + return result as Record; + } + } catch {} +}; + +/** + * 从通用 ChatBox 提交的 messages 中解析 HelperBot 运行输入。 + * + * HelperBot 不再定义单独的请求结构;Human 消息的 dataId、文本和文件 + * 都直接来自 ChatBox 的最后一条 user message,保证刷新后回显使用同一套记录字段。 + */ +export const parseHelperBotChatBoxMessages = (messages: ChatCompletionMessageParam[]) => { + const lastUserMessage = messages.findLast((message) => message.role === 'user'); + if (!lastUserMessage?.dataId) { + throw new Error('HelperBot human message dataId is empty'); + } + + const content = lastUserMessage.content; + const value: UserChatItemValueItemType[] = []; + if (typeof content === 'string') { + if (content) { + value.push({ + text: { + content + } + }); + } + + return { + userContent: { + dataId: lastUserMessage.dataId, + obj: ChatRoleEnum.Human as typeof ChatRoleEnum.Human, + hideInUI: lastUserMessage.hideInUI, + value + }, + query: content, + files: [] as HelperBotChatFileType[] + }; + } + + const files: HelperBotChatFileType[] = []; + const query = content + .map((item) => { + if (item.type === 'text') { + value.push({ + text: { + content: item.text + } + }); + return item.text; + } + + if (item.type === 'image_url') { + const file = { + type: ChatFileTypeEnum.image, + key: item.key || '', + name: '', + url: item.image_url.url + }; + files.push(file); + value.push({ + file: { + type: file.type, + name: file.name, + url: file.url, + key: file.key + } + }); + return; + } + + if (item.type === 'file_url') { + const file = { + type: (item.fileType as ChatFileTypeEnum | undefined) ?? ChatFileTypeEnum.file, + key: item.key || '', + name: item.name || '', + url: item.url + }; + files.push(file); + value.push({ + file: { + type: file.type, + name: file.name, + url: file.url, + key: file.key + } + }); + } + }) + .filter((text): text is string => typeof text === 'string') + .join('\n'); + + return { + userContent: { + dataId: lastUserMessage.dataId, + obj: ChatRoleEnum.Human as typeof ChatRoleEnum.Human, + hideInUI: lastUserMessage.hideInUI, + value + }, + query, + files + }; +}; + +type ResolveAuthorizedHelperBotFilesParams = { + files: HelperBotChatFileType[]; + sourceId: string; + uid: string; + chatId: string; + getPreviewUrl?: (key: string) => Promise; +}; + +/** + * 校验并解析 HelperBot 上传文件。 + * + * HelperBot completions 只能消费当前 helperBot chat source 下、当前成员上传的 chat file key。 + * 客户端传入的 url 不可信,这里统一丢弃并由服务端按授权后的 key 重新签预览链接。 + */ +export const resolveAuthorizedHelperBotFiles = async ({ + files, + sourceId, + uid, + chatId, + getPreviewUrl = createChatFilePreviewUrlGetter() +}: ResolveAuthorizedHelperBotFilesParams) => { + return Promise.all( + files.map(async (file) => { + if ( + !isAuthorizedChatFileS3Key({ + key: file.key, + sourceType: ChatSourceTypeEnum.helperBot, + sourceId, + uid, + chatId + }) + ) { + return Promise.reject(ChatErrEnum.unAuthChat); + } + + return { + ...file, + url: await getPreviewUrl(file.key) + }; + }) + ); +}; + +const applyUserInputInteractiveValue = ({ + aiValue, + query +}: { + aiValue: AIChatItemValueItemType[]; + query: string; +}) => { + const formData = parseUserInputValue(query); + if (!formData) return false; + + const updateInputForm = ( + inputForm: T[] + ) => + inputForm.map((item) => { + const value = formData[item.key]; + if (value === undefined) return item; + + if (item.type === FlowNodeInputTypeEnum.fileSelect && Array.isArray(value)) { + return { + ...item, + value: value.map((file) => + file && typeof file === 'object' + ? { + name: 'name' in file ? file.name : undefined, + url: 'url' in file ? file.url : undefined + } + : file + ) + }; + } + + return { + ...item, + value + }; + }); + + const interactiveIndex = aiValue.findLastIndex( + (item) => 'interactive' in item && !!item.interactive + ); + if (interactiveIndex === -1) return false; + + const interactiveValue = aiValue[interactiveIndex]; + if (!('interactive' in interactiveValue) || !interactiveValue.interactive) return false; + const interactive = extractDeepestInteractive(interactiveValue.interactive); + if (interactive.type !== 'userInput') return false; + + interactive.params.inputForm = updateInputForm(interactive.params.inputForm); + interactive.params.submitted = true; + return true; +}; + +/** + * 保存 HelperBot 一轮输出。 + * + * 普通新一轮的 pending Human/AI 由通用 preChatRound 创建,这里只负责 finalize。 + * interactive 续写不创建新 Human/AI item,而是回填原 AI item 的表单状态并 append 新响应。 + */ export const pushChatRecords = async ({ - type, - userId, + teamId, + tmbId, + sourceId, chatId, - chatItemId, + userContent, query, - files, + responseChatItemId, + shouldFinalizePreparedRound, aiResponse, + durationSeconds, memories, metadata }: { - type: HelperBotTypeEnum; - userId: string; + teamId: string; + tmbId: string; + sourceId: string; chatId: string; - chatItemId: string; + userContent: UserChatItemType & { dataId?: string }; query: string; - files: HelperBotCompletionsParamsType['files']; + responseChatItemId: string; + shouldFinalizePreparedRound: boolean; aiResponse: AIChatItemValueItemType[]; + durationSeconds?: number; memories?: Record; metadata?: Record; }) => { - const chat = await MongoHelperBotChat.findOne( + const now = new Date(); + const chatSource = { + sourceType: ChatSourceTypeEnum.helperBot, + sourceId + }; + + if (shouldFinalizePreparedRound) { + await finalizeChatRound({ + teamId, + tmbId, + sourceType: ChatSourceTypeEnum.helperBot, + sourceId, + chatId, + nodes: [], + source: ChatSourceEnum.test, + sourceName: 'HelperBot', + userContent, + aiContent: { + dataId: responseChatItemId, + obj: ChatRoleEnum.AI, + value: aiResponse, + memories + }, + durationSeconds: durationSeconds ?? 0, + metadata + }); + return; + } + + const chat = await MongoChat.findOne( { - type, - userId, + ...buildChatSourceQuery(chatSource), chatId }, '_id metadata' @@ -43,61 +301,78 @@ export const pushChatRecords = async ({ ...metadata }; - const userValue: UserChatItemValueItemType[] = [ - ...files.map((file) => ({ - file: { - type: file.type, - name: file.name, - url: '', - key: file.key || '' - } - })), - ...(query - ? [ - { - text: { - content: query - } - } - ] - : []) - ]; - await mongoSessionRun(async (session) => { - await MongoHelperBotChatItem.create( - [ - { - userId, - chatId, - dataId: chatItemId, - obj: ChatRoleEnum.Human, - value: userValue - }, + const persistCurrentRoundFiles = () => { + const contents: (UserChatItemType | AIChatItemType)[] = [ + userContent, { - userId, - chatId, - dataId: chatItemId, obj: ChatRoleEnum.AI, - value: aiResponse, - memories + value: aiResponse } - ], - { session, ordered: true } - ); + ]; - await MongoHelperBotChat.updateOne( + return persistChatFiles({ + contents, + session + }); + }; + + const existingAiItem = await MongoChatItem.findOne({ + ...buildChatSourceQuery(chatSource), + chatId, + dataId: responseChatItemId, + obj: ChatRoleEnum.AI + }).session(session); + + if (!existingAiItem) { + throw new Error(`HelperBot interactive chat item not found: ${chatId}/${responseChatItemId}`); + } + + const existingAiRecord = existingAiItem as typeof existingAiItem & { + value: AIChatItemValueItemType[]; + memories?: Record; + durationSeconds?: number; + }; + const nextValue = existingAiRecord.value; + const hasSubmittedInteractive = applyUserInputInteractiveValue({ + aiValue: nextValue, + query + }); + if (!hasSubmittedInteractive) { + throw new Error(`HelperBot interactive form not found: ${chatId}/${responseChatItemId}`); + } + + existingAiRecord.value = [...nextValue, ...aiResponse]; + existingAiRecord.memories = { + ...existingAiRecord.memories, + ...memories + }; + if (typeof durationSeconds === 'number') { + existingAiRecord.durationSeconds = existingAiRecord.durationSeconds + ? +(existingAiRecord.durationSeconds + durationSeconds).toFixed(2) + : durationSeconds; + } + existingAiRecord.markModified('value'); + existingAiRecord.markModified('memories'); + await existingAiRecord.save({ session }); + + await MongoChat.updateOne( { - type, - userId, + ...buildChatSourceQuery(chatSource), chatId }, { - updateTime: new Date(), - metadata: metadataUpdate + $set: { + updateTime: now, + metadata: metadataUpdate, + chatGenerateStatus: ChatGenerateStatusEnum.done, + hasBeenRead: false + } }, { session } ); + await persistCurrentRoundFiles(); }); }; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index dae59be0f14a..94aea5c7d4bb 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -69,7 +69,14 @@ const beforeProcess = (props: Props) => { const isFileValueWithKey = (file: unknown): file is { key: string } => !!file && typeof file === 'object' && 'key' in file && typeof file.key === 'string' && !!file.key; -const afterProcess = async ({ +/** + * 对话内容落库成功后,移除已持久化文件的 S3 TTL 记录。 + * + * ChatBox 上传文件时会先写入临时 TTL,避免用户只上传不发送时文件长期残留。 + * 当包含这些 file key 的消息、工作流表单值或文件变量已经进入对话记录后, + * 这里统一删除 TTL,表示这些文件跟随对话生命周期持久化。 + */ +export const persistChatFiles = async ({ contents, variables, variableList, @@ -363,7 +370,7 @@ export const finalizeChatRound = async (props: Props) => { } ); - await afterProcess({ + await persistChatFiles({ contents: processedContent, variables, variableList, @@ -584,7 +591,8 @@ export const pushChatRecords = async (props: Props) => { shareId, outLinkUid, metadata: metadataUpdate, - updateTime: new Date() + updateTime: new Date(), + chatGenerateStatus: ChatGenerateStatusEnum.done }, $setOnInsert: { createTime: new Date() @@ -598,7 +606,7 @@ export const pushChatRecords = async (props: Props) => { } ); - await afterProcess({ + await persistChatFiles({ contents: processedContent, variables, variableList, @@ -890,7 +898,7 @@ export const updateInteractiveChat = async ({ } ); - await afterProcess({ + await persistChatFiles({ contents: [userContent, aiContent], variables, variableList, diff --git a/packages/service/core/chat/source.ts b/packages/service/core/chat/source.ts index d4d9527ee947..596ad2e2ec4a 100644 --- a/packages/service/core/chat/source.ts +++ b/packages/service/core/chat/source.ts @@ -22,8 +22,8 @@ export function buildChatSourceWriteFields({ sourceType, sourceId }: ChatSourceP /** * 构造 chat 三表 source-aware 查询条件。 * - * App 查询默认兼容缺失 `sourceType` 的历史数据;Skill Edit 必须精确匹配 - * `sourceType=skillEdit`。 + * App 查询默认兼容缺失 `sourceType` 的历史数据;非 App 来源必须精确匹配 + * `sourceType`,避免不同资源类型复用同一物理 `appId` 字段时串记录。 */ export function buildChatSourceQuery({ sourceType, sourceId }: ChatSourceParams) { if (sourceType === ChatSourceTypeEnum.app) { @@ -33,10 +33,10 @@ export function buildChatSourceQuery({ sourceType, sourceId }: ChatSourceParams) }; } - if (sourceType === ChatSourceTypeEnum.skillEdit) { + if (sourceType === ChatSourceTypeEnum.skillEdit || sourceType === ChatSourceTypeEnum.helperBot) { return { appId: sourceId, - sourceType: ChatSourceTypeEnum.skillEdit + sourceType }; } diff --git a/packages/service/core/workflow/dispatch/ai/agent/adapter/userContext.ts b/packages/service/core/workflow/dispatch/ai/agent/adapter/userContext.ts index b5c33bda2dc1..3e831eb57799 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/adapter/userContext.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/adapter/userContext.ts @@ -119,11 +119,12 @@ export function parseAgentInputFiles({ .map(({ file, url }, index) => { const parsedFile = parseUrlToFileType(url); if (!parsedFile) return; + const type = file.type && file.type !== ChatFileTypeEnum.file ? file.type : parsedFile.type; return { id: `${prefixId}-${index}`, name: getSafeSandboxInputFilename(file.name || parsedFile.name || url, index, usedNames), - type: parsedFile.type, + type, url: parsedFile.url }; }) @@ -293,6 +294,15 @@ ${currentTime ? `当前时间: ${currentTime}` : ''} ${currentWorkingDirectory ? `当前 sandbox 工作目录: ${currentWorkingDirectory}` : ''}`; }; +const getAgentMultimodalChatFiles = (files: AgentInputFile[]) => + files + .filter((file) => file.type !== ChatFileTypeEnum.file) + .map(({ name, type, url }) => ({ + name, + type: type as ChatFileTypeEnum, + url + })); + const buildSandboxFileWriteBoundaryPrompt = ({ currentWorkingDirectory }: { @@ -437,6 +447,7 @@ export const useUserContext = async ({ return { ...message, value: runtimePrompt2ChatsValue({ + files: getAgentMultimodalChatFiles(formatFiles), text: buildAgentUserReminderInput({ query: text, filesInfo: formatFiles @@ -479,6 +490,7 @@ export const useUserContext = async ({ const currentUserMessage: ChatItemMiniType = { ...currentMessage, value: runtimePrompt2ChatsValue({ + files: getAgentMultimodalChatFiles(currentInputFiles), // 当前 Human 才注入完整 reminder:sandbox、skill、文件、知识库、当前时间和原始问题。 text: buildAgentUserReminderInput({ query: currentUserInput, diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 11d06a5ae4b2..677b9d2dfdeb 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -9,6 +9,7 @@ import type { ChatHistoryItemResType, ChatItemMiniType } from '@fastgpt/global/core/chat/type'; +import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { getSystemToolInfo } from '@fastgpt/global/core/workflow/node/agent/constants'; import { SANDBOX_SYSTEM_PROMPT } from '@fastgpt/global/core/ai/sandbox/constants'; @@ -177,6 +178,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise effectiveUseAgentSandbox && useAgentSandbox && global.feConfigs?.show_agent_sandbox ? sandboxEntrypoint : undefined; + const skipSandboxInputFiles = runningAppInfo.sourceType === ChatSourceTypeEnum.skillEdit; // 初始化对话框输入的文件 const fileUrlInput = inputs.find((item) => item.key === NodeInputKeyEnum.fileUrlList); @@ -229,7 +231,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise selectedSkills, editSkillId, prepareActions: agentSandboxPrepareActions, - currentFiles: userContext.currentFiles + currentFiles: skipSandboxInputFiles ? [] : userContext.currentFiles }); // 获取请求上下文 const { chatHistories, queryInput, filesMap, fileUrlMap } = userContext; diff --git a/packages/service/core/workflow/dispatch/ai/agent/utils.ts b/packages/service/core/workflow/dispatch/ai/agent/utils.ts index 344c7ed74428..f67bdbf06e7c 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/utils.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/utils.ts @@ -284,11 +284,10 @@ export const getExecuteTool = ({ }; } const ids = toolParams.data.ids; - - const files = ids.map((id) => ({ - id, - url: filesMap[id] - })); + const files = ids.flatMap((id) => { + const url = filesMap[id]; + return url ? [{ id, url }] : []; + }); const result = await dispatchFileRead({ files, teamId: runningUserInfo.teamId, diff --git a/packages/service/env.ts b/packages/service/env.ts index afda4b7671eb..86c1d1b5dbb9 100644 --- a/packages/service/env.ts +++ b/packages/service/env.ts @@ -244,10 +244,6 @@ export const serviceEnv = createEnv({ AGENT_ENGINE: z.enum(['default', 'pi']).default('default').meta({ description: 'Agent 引擎选择:default(unified agent loop)| pi(pi-agent-core 引擎)' }), - HELPER_BOT_MODEL: z - .string() - .optional() - .meta({ description: '辅助生成模型(暂时只能指定一个,需保证系统中已激活该模型)' }), SKIP_FILE_TYPE_CHECK: BoolSchema.default(false).meta({ description: '是否跳过文件类型检查' }), diff --git a/packages/service/support/permission/teamLimit.ts b/packages/service/support/permission/teamLimit.ts index a1169dbc32fe..54b49fb747d0 100644 --- a/packages/service/support/permission/teamLimit.ts +++ b/packages/service/support/permission/teamLimit.ts @@ -175,6 +175,8 @@ export const checkTeamDatasetSyncPermission = async (teamId: string) => { }; export const checkTeamSandboxPermission = async (teamId: string) => { + if (!global.subPlans?.standard) return; + const { standard } = await getTeamStandPlan({ teamId }); diff --git a/packages/service/support/wallet/sub/utils.ts b/packages/service/support/wallet/sub/utils.ts index 6fe1d841ab49..5dd1ee50f118 100644 --- a/packages/service/support/wallet/sub/utils.ts +++ b/packages/service/support/wallet/sub/utils.ts @@ -160,6 +160,7 @@ export const initTeamFreePlan = async ({ // 获取团队标准套餐 export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { + const standardPlans = global.subPlans?.standard; const plans = await MongoTeamSub.find( { teamId, @@ -172,11 +173,10 @@ export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { ).lean(); sortStandPlans(plans); - const standardPlans = global.subPlans?.standard; const standard = plans[0]; const standardConstants = - standard.currentSubLevel && standardPlans + standard?.currentSubLevel && standardPlans ? standardPlans[ standard.currentSubLevel === StandardSubLevelEnum.custom ? StandardSubLevelEnum.advanced @@ -185,9 +185,8 @@ export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { : undefined; return { - [SubTypeEnum.standard]: standardConstants - ? buildStandardPlan(standard, standardConstants) - : undefined + [SubTypeEnum.standard]: + standard && standardConstants ? buildStandardPlan(standard, standardConstants) : undefined }; }; diff --git a/packages/service/test/common/s3/key.test.ts b/packages/service/test/common/s3/key.test.ts index 40972e057388..21e18011e10d 100644 --- a/packages/service/test/common/s3/key.test.ts +++ b/packages/service/test/common/s3/key.test.ts @@ -7,10 +7,6 @@ import { isAuthorizedDatasetFileS3Key, parseDatasetFileS3Key } from '@fastgpt/service/common/s3/sources/dataset/key'; -import { - isAuthorizedHelperBotFileS3Key, - parseHelperBotFileS3Key -} from '@fastgpt/service/common/s3/sources/helperbot/key'; import { isAuthorizedTempFileS3Key } from '@fastgpt/service/common/s3/sources/temp/key'; describe('authorized S3 object key helpers', () => { @@ -125,20 +121,6 @@ describe('authorized S3 object key helpers', () => { expect(parseDatasetFileS3Key('dataset/dataset-1')).toBeNull(); }); - it('parses and authorizes helper bot file keys by userId', () => { - const key = 'helperBot/topAgent/user-1/chat-1/demo.pdf'; - - expect(parseHelperBotFileS3Key(key)).toEqual({ - type: 'topAgent', - userId: 'user-1', - chatId: 'chat-1', - filename: 'demo.pdf' - }); - expect(isAuthorizedHelperBotFileS3Key({ key, userId: 'user-1' })).toBe(true); - expect(isAuthorizedHelperBotFileS3Key({ key, userId: 'user-2' })).toBe(false); - expect(parseHelperBotFileS3Key('helperBot/unknown/user-1/chat-1/demo.pdf')).toBeNull(); - }); - it('authorizes temp file keys by exact team path segment', () => { expect(isAuthorizedTempFileS3Key({ key: 'temp/team-1/demo.pdf', teamId: 'team-1' })).toBe(true); expect(isAuthorizedTempFileS3Key({ key: 'temp/team-11/demo.pdf', teamId: 'team-1' })).toBe( diff --git a/packages/service/test/core/chat/saveChat.test.ts b/packages/service/test/core/chat/saveChat.test.ts index b1586eaf945c..a6d1fdf4db3f 100644 --- a/packages/service/test/core/chat/saveChat.test.ts +++ b/packages/service/test/core/chat/saveChat.test.ts @@ -13,6 +13,8 @@ import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoAppChatLog } from '@fastgpt/service/core/app/logs/chatLogsSchema'; import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema'; +import { MongoS3TTL } from '@fastgpt/service/common/s3/models/ttl'; +import { S3Buckets } from '@fastgpt/service/common/s3/config/constants'; import { ChatFileTypeEnum, ChatGenerateStatusEnum, @@ -606,6 +608,103 @@ describe('pushChatRecords', () => { expect(aiItem?.responseData).toBeUndefined(); }); + it('should persist skill edit uploaded files by removing S3 TTL after finalization', async () => { + const responseChatItemId = 'skill-edit-file-finalize'; + const fileKey = `chat/${ChatSourceTypeEnum.skillEdit}/${testAppId}/${testTmbId}/skill-chat-id/spec.md`; + const props = createMockProps( + { + chatId: 'skill-chat-id', + sourceType: ChatSourceTypeEnum.skillEdit, + sourceId: testAppId, + userContent: { + dataId: responseChatItemId, + obj: ChatRoleEnum.Human, + value: [ + { + file: { + type: ChatFileTypeEnum.file, + name: 'spec.md', + url: 'https://example.com/temp-preview', + key: fileKey + } + } + ] + }, + aiContent: { + dataId: responseChatItemId, + obj: ChatRoleEnum.AI, + value: [ + { + text: { + content: 'Skill debug answer' + } + } + ] + } + }, + { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } + ); + + await MongoS3TTL.create({ + bucketName: S3Buckets.private, + minioKey: fileKey, + expiredTime: new Date(Date.now() + 60 * 60 * 1000) + }); + await MongoChat.create({ + appId: testAppId, + chatId: props.chatId, + teamId: testTeamId, + tmbId: testTmbId, + sourceType: ChatSourceTypeEnum.skillEdit, + source: props.source, + chatGenerateStatus: ChatGenerateStatusEnum.generating, + hasBeenRead: false + }); + await MongoChatItem.create([ + { + teamId: testTeamId, + tmbId: testTmbId, + sourceType: ChatSourceTypeEnum.skillEdit, + appId: testAppId, + chatId: props.chatId, + dataId: responseChatItemId, + obj: ChatRoleEnum.Human, + value: [] + }, + { + teamId: testTeamId, + tmbId: testTmbId, + sourceType: ChatSourceTypeEnum.skillEdit, + appId: testAppId, + chatId: props.chatId, + dataId: responseChatItemId, + obj: ChatRoleEnum.AI, + value: [] + } + ]); + + await finalizeChatRound(props); + + const ttl = await MongoS3TTL.findOne({ + bucketName: S3Buckets.private, + minioKey: fileKey + }).lean(); + expect(ttl).toBeNull(); + + const humanItem = await MongoChatItem.findOne({ + sourceType: ChatSourceTypeEnum.skillEdit, + appId: testAppId, + chatId: props.chatId, + obj: ChatRoleEnum.Human + }).lean(); + expect(humanItem?.value[0].file).toEqual({ + type: ChatFileTypeEnum.file, + name: 'spec.md', + url: '', + key: fileKey + }); + }); + it('should mark prepared round as error and keep ai placeholder', async () => { const responseChatItemId = 'prepared-ai-error'; const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId }); diff --git a/packages/service/test/core/workflow/dispatch/ai/agent/adapter/userContext.test.ts b/packages/service/test/core/workflow/dispatch/ai/agent/adapter/userContext.test.ts index 47ca4321ec77..13eaa49efc6f 100644 --- a/packages/service/test/core/workflow/dispatch/ai/agent/adapter/userContext.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/agent/adapter/userContext.test.ts @@ -211,6 +211,28 @@ describe('parseAgentInputFiles', () => { ]); expect(files.every((file) => !file.name.includes('/'))).toBe(true); }); + + it('keeps explicit image type when preview url has no file extension', () => { + const files = parseAgentInputFiles({ + files: [ + { + name: '20260629-233136_wT851P.jpg', + type: ChatFileTypeEnum.image, + url: 'https://fastgpt.example.com/api/core/chat/file/preview?token=abc' + } + ], + prefixId: 'current', + maxFiles: 10 + }); + + expect(files).toEqual([ + expect.objectContaining({ + name: '20260629-233136_wT851P.jpg', + type: ChatFileTypeEnum.image, + url: 'https://fastgpt.example.com/api/core/chat/file/preview?token=abc' + }) + ]); + }); }); describe('buildAgentUserReminderInput', () => { @@ -412,6 +434,7 @@ describe('useUserContext', () => { { queryUrlTypeMap: { '/old.pdf': ChatFileTypeEnum.file, + '/old.png': ChatFileTypeEnum.image, '/current.pdf': ChatFileTypeEnum.file, '/current.png': ChatFileTypeEnum.image }, @@ -421,7 +444,10 @@ describe('useUserContext', () => { const history = createHumanMessage({ dataId: 'history_1', text: '历史问题', - files: [{ name: 'old.pdf', url: '/old.pdf' }] + files: [ + { name: 'old.pdf', url: '/old.pdf' }, + { name: 'old.png', url: '/old.png', type: ChatFileTypeEnum.image } + ] }); const result = await getUserContextMessagesForTest({ history: 6, @@ -452,19 +478,36 @@ describe('useUserContext', () => { }); expect(result.fileUrlMap).toEqual({ 'history_1-0': '/old.pdf', + 'history_1-1': '/old.png', 'current_chat_item-0': '/current.pdf', 'current_chat_item-1': '/current.png' }); - const { text: historyText } = chatValue2RuntimePrompt(result.rewrittenHistories[0].value); + const { text: historyText, files: historyFiles } = chatValue2RuntimePrompt( + result.rewrittenHistories[0].value + ); const { text: currentText, files: currentFiles } = chatValue2RuntimePrompt( result.currentUserMessage.value ); + expect(historyFiles).toEqual([ + { + name: 'old.png', + type: ChatFileTypeEnum.image, + url: '/old.png' + } + ]); expect(historyText).toContain('history_1-0'); + expect(historyText).toContain('history_1-1'); expect(historyText).not.toContain('当前 sandbox 工作目录'); expect(historyText).not.toContain('当前时间'); - expect(currentFiles).toEqual([]); + expect(currentFiles).toEqual([ + { + name: 'current.png', + type: ChatFileTypeEnum.image, + url: '/current.png' + } + ]); expect(currentText).toContain('## 背景信息'); expect(currentText).toContain('当前 sandbox 工作目录: /workspace'); expect(currentText).toContain('current_chat_item-0'); @@ -724,6 +767,47 @@ describe('useUserContext', () => { ); }); + it('keeps uploaded image as multimodal content when preview url has no extension', async () => { + await runWithContextAsync( + { + queryUrlTypeMap: {}, + mcpClientMemory: {} + }, + async () => { + const previewUrl = 'https://fastgpt.example.com/api/core/chat/file/preview?token=abc'; + const result = await getUserContextMessagesForTest({ + history: 0, + histories: [], + currentUserInput: '看一下这张图', + currentDataId: 'current_chat_item', + currentQuery: runtimePrompt2ChatsValue({ + text: '前端原始问题', + files: [ + { + name: '20260629-233136_wT851P.jpg', + url: previewUrl, + type: ChatFileTypeEnum.image + } + ] + }), + tmbId: 'tmb_1', + timezone: 'Asia/Shanghai', + maxFiles: 20 + }); + + const { files, text } = chatValue2RuntimePrompt(result.currentUserMessage.value); + expect(files).toEqual([ + expect.objectContaining({ + type: ChatFileTypeEnum.image, + name: '20260629-233136_wT851P.jpg', + url: previewUrl + }) + ]); + expect(text).toContain('image'); + } + ); + }); + it('uses message index when historical human has no human dataId', async () => { await runWithContextAsync( { diff --git a/packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts b/packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts index 1f9d8dd8ca7b..68117163c6ad 100644 --- a/packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts @@ -509,6 +509,48 @@ describe('dispatchRunAgent user context', () => { expect(loopInput.messages.at(-1)?.content).toContain('./SKILL.md'); }); + it('does not inject current files into sandbox for skill edit preview', async () => { + const { dispatchRunAgent } = await import('@fastgpt/service/core/workflow/dispatch/ai/agent'); + const props = createProps(); + props.runningAppInfo.sourceType = ChatSourceTypeEnum.skillEdit; + props.runningAppInfo.sourceId = 'edit_skill_1'; + props.params.editSkillId = 'edit_skill_1'; + ensureAgentSandboxRuntimeMock.mockResolvedValueOnce({ + sandboxClient: { + provider: { + writeFiles: vi.fn() + }, + exec: sandboxClientExecMock, + getSandboxId: () => 'sandbox_prepared' + }, + currentWorkingDirectory: '/workspace', + skillInfos: [] + }); + + let result: any; + runWithContext( + { + queryUrlTypeMap: { + '/current.pdf': ChatFileTypeEnum.file + }, + mcpClientMemory: {} + }, + () => { + result = dispatchRunAgent(props); + } + ); + await result; + + expect(ensureAgentSandboxRuntimeMock).toHaveBeenCalledWith( + expect.objectContaining({ + sourceType: ChatSourceTypeEnum.skillEdit, + sourceId: 'edit_skill_1', + editSkillId: 'edit_skill_1', + currentFiles: [] + }) + ); + }); + it('returns the final answer as assistant response', async () => { const { dispatchRunAgent } = await import('@fastgpt/service/core/workflow/dispatch/ai/agent'); diff --git a/packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts b/packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts index 74da7a716370..3fcd57c32af3 100644 --- a/packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts @@ -23,7 +23,6 @@ const { ensureAgentSandboxRuntimeMock, streamAgentSandboxInitStatusMock, sandboxClientExecMock, - axiosGetMock, getAgentRuntimeToolsMock } = vi.hoisted(() => ({ agentPromptMock: vi.fn(), @@ -37,7 +36,6 @@ const { ensureAgentSandboxRuntimeMock: vi.fn(), streamAgentSandboxInitStatusMock: vi.fn(), sandboxClientExecMock: vi.fn(), - axiosGetMock: vi.fn(), getAgentRuntimeToolsMock: vi.fn(async () => []) })); @@ -87,18 +85,6 @@ vi.mock('@fastgpt/service/core/workflow/dispatch/ai/agent/sub/sandbox', async (i }; }); -vi.mock('@fastgpt/service/common/api/axios', async (importOriginal) => { - const original = await importOriginal(); - const mockClient = { - get: axiosGetMock - }; - - return { - ...original, - pickOutboundAxios: () => mockClient - }; -}); - vi.mock('@fastgpt/service/core/dataset/schema', async (importOriginal) => { const original = await importOriginal(); return { @@ -256,9 +242,6 @@ describe('dispatchPiAgent user context', () => { stdout: '/workspace\n', stderr: '' }); - axiosGetMock.mockResolvedValue({ - data: new ArrayBuffer(1) - }); ensureAgentSandboxRuntimeMock.mockResolvedValue({ sandboxClient: { provider: { diff --git a/packages/service/test/core/workflow/dispatch/ai/agent/utils.test.ts b/packages/service/test/core/workflow/dispatch/ai/agent/utils.test.ts index 203fea75df52..48eb6d3c28e1 100644 --- a/packages/service/test/core/workflow/dispatch/ai/agent/utils.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/agent/utils.test.ts @@ -163,6 +163,59 @@ describe('Agent read_files tool protocol', () => { ); }); + it('ignores read_files ids that are not registered document files', async () => { + const executeTool = getExecuteTool({ + checkIsStopping: vi.fn(), + chatConfig: {}, + runningUserInfo: { + teamId: 'team_1', + tmbId: 'tmb_1' + }, + runningAppInfo: { + id: 'app_1' + }, + chatId: 'chat_1', + uid: 'user_1', + variableState: {} as any, + externalProvider: { + openaiAccount: undefined + } as any, + lang: 'zh-CN', + requestOrigin: '', + mode: 'chat', + timezone: 'Asia/Shanghai', + retainDatasetCite: false, + maxRunTimes: 10, + workflowDispatchDeep: 0, + params: { + model: 'gpt-4' + }, + stream: false, + getSubAppInfo: () => ({ + name: '文件解析', + avatar: '', + toolDescription: '' + }), + getSubApp: () => undefined, + completionTools: [readFileTool], + filesMap: {} + } as any); + + const result = await executeTool({ + callId: 'call_read_files', + toolId: SubAppIds.readFiles, + args: '{"ids":["current-0"]}' + }); + + expect(dispatchFileReadMock).toHaveBeenCalledTimes(1); + expect(dispatchFileReadMock).toHaveBeenCalledWith( + expect.objectContaining({ + files: [] + }) + ); + expect(result.response).toBe('file content'); + }); + it('replaces agent file ids before dispatching user tools', async () => { dispatchToolMock.mockResolvedValue({ response: 'tool response', diff --git a/packages/service/test/support/permission/teamLimit.test.ts b/packages/service/test/support/permission/teamLimit.test.ts index a16f8d36d95a..df76111fc6fa 100644 --- a/packages/service/test/support/permission/teamLimit.test.ts +++ b/packages/service/test/support/permission/teamLimit.test.ts @@ -836,9 +836,22 @@ describe('checkTeamDatasetSyncPermission', () => { describe('checkTeamSandboxPermission', () => { beforeEach(() => { vi.clearAllMocks(); + delete (global as any).subPlans; + }); + + it('当 global.subPlans.standard 不存在时直接返回', async () => { + const getTeamStandPlanSpy = vi.spyOn(walletUtils, 'getTeamStandPlan'); + + await expect(checkTeamSandboxPermission(mockTeamId)).resolves.toBeUndefined(); + expect(getTeamStandPlanSpy).not.toHaveBeenCalled(); }); it('当 enableSandbox 为 false 时抛出错误', async () => { + (global as any).subPlans = { + standard: { + [StandardSubLevelEnum.basic]: {} + } + }; const mockStandard = { standard: { enableSandbox: false @@ -852,6 +865,11 @@ describe('checkTeamSandboxPermission', () => { }); it('当 standard 不存在时不抛出错误', async () => { + (global as any).subPlans = { + standard: { + [StandardSubLevelEnum.basic]: {} + } + }; const mockStandard = { standard: undefined }; @@ -861,6 +879,11 @@ describe('checkTeamSandboxPermission', () => { }); it('当 enableSandbox 为 true 时正常通过', async () => { + (global as any).subPlans = { + standard: { + [StandardSubLevelEnum.basic]: {} + } + }; const mockStandard = { standard: { enableSandbox: true diff --git a/packages/service/test/support/wallet/sub/utils.test.ts b/packages/service/test/support/wallet/sub/utils.test.ts index b30ba87ce406..5dde939199dc 100644 --- a/packages/service/test/support/wallet/sub/utils.test.ts +++ b/packages/service/test/support/wallet/sub/utils.test.ts @@ -678,6 +678,16 @@ describe('getTeamStandPlan', () => { delete (global as any).subPlans; }); + it('未开启标准套餐配置且没有套餐记录时返回 undefined', async () => { + vi.spyOn(MongoTeamSub, 'find').mockReturnValue({ + lean: vi.fn().mockResolvedValue([]) + } as any); + + const result = await getTeamStandPlan({ teamId: mockTeamId }); + + expect(result[SubTypeEnum.standard]).toBeUndefined(); + }); + it('返回团队标准套餐', async () => { const teamId = mockTeamId; const mockPlan = { @@ -722,6 +732,32 @@ describe('getTeamStandPlan', () => { expect(result[SubTypeEnum.standard]).toBeDefined(); expect(result[SubTypeEnum.standard]?.name).toBe('Basic Plan'); }); + + it('开启标准套餐配置但没有套餐记录时返回 undefined', async () => { + vi.spyOn(MongoTeamSub, 'find').mockReturnValue({ + lean: vi.fn().mockResolvedValue([]) + } as any); + + (global as any).subPlans = { + standard: { + [StandardSubLevelEnum.free]: { + name: 'Free Plan', + price: 0, + totalPoints: 100, + maxTeamMember: 1, + maxAppAmount: 3, + maxDatasetAmount: 1, + maxDatasetSize: 10, + chatHistoryStoreDuration: 7, + enableSandbox: true + } + } + }; + + const result = await getTeamStandPlan({ teamId: mockTeamId }); + + expect(result[SubTypeEnum.standard]).toBeUndefined(); + }); }); describe('getTeamPlanStatus', () => { diff --git a/pro b/pro index 32ff3fa6772f..a27a8aa13daf 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 32ff3fa6772fa67463ad64a080a568e620433e8f +Subproject commit a27a8aa13daf26327c131c334bdf1916bd37862c diff --git a/projects/app/.env.template b/projects/app/.env.template index 3526593d82e0..138127f21b84 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -186,8 +186,6 @@ OPENAPI_KEY_MAX_COUNT=100 # Agent 引擎选择:default(Plan+Step 编排)| pi(pi-agent-core 引擎) AGENT_ENGINE=default -# 辅助生成模型(暂时只能指定一个,需保证系统中已激活该模型) -HELPER_BOT_MODEL=qwen-max # 对话标题生成模型(不填则使用默认 LLM 模型) CHAT_TITLE_MODEL= SKIP_FILE_TYPE_CHECK=false diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/index.tsx index 9e37016cd3f7..66a743a9c78f 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/index.tsx @@ -11,13 +11,9 @@ import { useContextSelector } from 'use-context-selector'; import { ChatBoxContext } from '../../Provider'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import AIChatLoading from '../AIChatLoading'; -import { hasAiProcessingContent } from './utils'; +import { hasAiProcessingContent, shouldShowNoOutputTip } from './utils'; import { useTranslation } from 'next-i18next'; -import { - ChatGenerateStatusEnum, - ChatRoleEnum, - ChatStatusEnum -} from '@fastgpt/global/core/chat/constants'; +import { ChatGenerateStatusEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants'; const ResponseTags = dynamic(() => import('../ResponseTags')); const WholeResponseModal = dynamic(() => import('../../../../components/WholeResponseModal')); @@ -85,15 +81,16 @@ const AIChatBubble = ({ !chat.errorMsg && !chat.errorText && !hasValidContent; - const showNoOutputTip = - chat.obj === ChatRoleEnum.AI && - chat.status === ChatStatusEnum.finish && - isLastValueGroup && - !isChatting && - !shouldWaitCurrentChatStatus && - !chat.errorMsg && - !chat.errorText && - !hasValidContent; + const showNoOutputTip = shouldShowNoOutputTip({ + obj: chat.obj, + status: chat.status, + isLastValueGroup, + isLastChild, + isChatting, + shouldWaitCurrentChatStatus, + hasError: !!chat.errorMsg || !!chat.errorText, + hasValidContent + }); const placeholderText = showLoadingPlaceholder ? t('chat:chat_loading_content') : showNoOutputTip diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/utils.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/utils.ts index c52884e09fb8..2bf9f8439614 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/utils.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/AIChatBubble/utils.ts @@ -1,4 +1,5 @@ import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; +import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants'; export const hasAiAnswerContent = (item: AIChatItemValueItemType) => Boolean(item.text?.content?.trim()); @@ -11,11 +12,11 @@ export const hasAiProcessingContent = (item: AIChatItemValueItemType) => { const tools = item.tools || (item.tool ? [item.tool] : undefined); return Boolean( (item.reasoning?.content && !item.hideReason) || - (item.agentPlanUpdate?.reasoningText && !item.hideReason) || - tools?.length || - item.skills?.length || - item.plan || - item.planStatus?.status === 'generating' + (item.agentPlanUpdate?.reasoningText && !item.hideReason) || + tools?.length || + item.skills?.length || + item.plan || + item.planStatus?.status === 'generating' ); }; @@ -25,17 +26,15 @@ export const hasAiFoldableProcessingContent = (item: AIChatItemValueItemType) => const tools = item.tools || (item.tool ? [item.tool] : undefined); return Boolean( (item.reasoning?.content && !item.hideReason) || - (item.agentPlanUpdate?.reasoningText && !item.hideReason) || - tools?.length + (item.agentPlanUpdate?.reasoningText && !item.hideReason) || + tools?.length ); }; export const hasAiStandaloneProcessingContent = (item: AIChatItemValueItemType) => { if (item.hideInUI) return false; - return Boolean( - item.skills?.length || item.plan || item.planStatus?.status === 'generating' - ); + return Boolean(item.skills?.length || item.plan || item.planStatus?.status === 'generating'); }; export const shouldFilterAiValue = (item: AIChatItemValueItemType) => { @@ -56,3 +55,36 @@ export const shouldFilterAiValue = (item: AIChatItemValueItemType) => { item.contextCheckpoint ); }; + +/** + * 判断 AI 气泡是否需要展示“应用无输出内容”兜底提示。 + * + * `isChatting` 是 ChatBox 级别的全局状态,历史消息在下一轮生成时也会收到 true。 + * 因此只能在当前气泡自身是最后一条且正在生成时隐藏提示,避免历史空输出提示被后续发送误隐藏。 + */ +export const shouldShowNoOutputTip = ({ + obj, + status, + isLastValueGroup, + isLastChild, + isChatting, + shouldWaitCurrentChatStatus, + hasError, + hasValidContent +}: { + obj: `${ChatRoleEnum}`; + status: `${ChatStatusEnum}`; + isLastValueGroup: boolean; + isLastChild: boolean; + isChatting: boolean; + shouldWaitCurrentChatStatus: boolean; + hasError: boolean; + hasValidContent: boolean; +}) => + obj === ChatRoleEnum.AI && + status === ChatStatusEnum.finish && + isLastValueGroup && + !(isLastChild && isChatting) && + !shouldWaitCurrentChatStatus && + !hasError && + !hasValidContent; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatGenerate.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatGenerate.ts index 050dfdb2fd58..277cd751ed34 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatGenerate.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatGenerate.ts @@ -721,6 +721,7 @@ export const useChatGenerate = ({ const { responseText } = await onStartChat({ messages, responseChatItemId: responseChatId, + interactive, controller: abortSignal, generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }), variables: requestVariables diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatRecordActions.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatRecordActions.ts index 4c2f1d9a9867..b7759f26a317 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatRecordActions.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useChatRecordActions.ts @@ -25,7 +25,7 @@ const uniqueDataIds = (dataIds: Array) => * - `retryInput` 会删除目标 human 消息及其后续记录,再用旧输入重新发送。 * - `editInput` 会删除目标 human 消息及其后续记录,再用编辑后的文本重新发送。 * - `delOneMessage` 会删除一条 human 消息,并顺带删除紧随其后的 AI 回复。 - * - `onDelMessage` 是 hook 内部的删除通道,统一使用标准 `delChatRecordById` API, + * - `onDelMessages` 是 hook 内部的删除通道,统一使用标准 `delChatRecordById` API, * 并自动带上当前 chat target、chatId 和外链鉴权信息。 * * 设计边界: diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx index 713e2f77847a..6b27b51fd053 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx @@ -191,7 +191,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => { // Get Upload Post Presigned URL const { url, key, headers, maxSize, previewUrl } = await getUploadChatFilePresignedUrl({ - filename: copyFile.rawFile.name, + filename: copyFile.rawFile!.name, ...chatAuthTarget, chatId, fileSelectConfig @@ -236,6 +236,8 @@ export const useFileUpload = (props: UseFileUploadOptions) => { fileSelectConfig, removeFiles, replaceFiles, + sourceTarget.sourceId, + sourceTarget.sourceType, t, toast, updateFiles diff --git a/projects/app/src/components/core/chat/ChatContainer/type.ts b/projects/app/src/components/core/chat/ChatContainer/type.ts index d2d32d445ef9..31851cc3ad5f 100644 --- a/projects/app/src/components/core/chat/ChatContainer/type.ts +++ b/projects/app/src/components/core/chat/ChatContainer/type.ts @@ -7,10 +7,7 @@ import type { SkillModuleResponseItemType } from '@fastgpt/global/core/chat/type'; import type { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; -import type { - UserInputInteractive, - WorkflowInteractiveResponseType -} from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import type { TopAgentFormDataType } from '@fastgpt/global/core/chat/helperBot/topAgent/type'; import type { AgentPlanStatusType, AgentPlanType } from '@fastgpt/global/core/ai/agent/type'; @@ -37,14 +34,13 @@ export type generatingMessageProps = { sandboxStatus?: SandboxStatusItemType; skill?: SkillModuleResponseItemType; - // HelperBot - collectionForm?: UserInputInteractive; formData?: TopAgentFormDataType; }; export type StartChatFnProps = { messages: ChatCompletionMessageParam[]; responseChatItemId?: string; + interactive?: WorkflowInteractiveResponseType; controller: AbortController; variables: Record; generatingMessage: (e: generatingMessageProps) => void; diff --git a/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx b/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx deleted file mode 100644 index 26fbdc1c9388..000000000000 --- a/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx +++ /dev/null @@ -1,404 +0,0 @@ -import type { FlexProps } from '@chakra-ui/react'; -import { Box, Flex, Textarea, useBoolean } from '@chakra-ui/react'; -import React, { 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 { type ChatBoxInputFormType } from '../ChatContainer/ChatBox/type'; -import { ChatInputDefaultHeight, textareaMinH } from '../ChatContainer/ChatBox/constants'; -import { useFieldArray, type UseFormReturn } from 'react-hook-form'; -import { useContextSelector } from 'use-context-selector'; -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; - chatForm: UseFormReturn; - 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; - const isDefaultInputHeight = !inputValue && fileList.length === 0; - - /* 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( - () => ( - 0 ? 1 : 0}> - {/* Textarea */} - - {/* Prompt Container */} -