From 80409259c3b67a7911826f98bd6ed4b3f332d63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Tue, 3 Mar 2026 16:36:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Bot=E5=8F=91=E5=B8=83=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=A4=96=E9=93=BE=E8=81=8A=E5=A4=A9=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/SysTempTokenController.java | 2 + .../listener/ChatStreamListener.java | 45 + .../ai/service/impl/BotServiceImpl.java | 11 +- .../app/src/locales/langs/en-US/bot.json | 34 +- .../locales/langs/en-US/settingsConfig.json | 4 +- .../app/src/locales/langs/zh-CN/bot.json | 34 +- .../locales/langs/zh-CN/settingsConfig.json | 4 +- .../app/src/router/routes/core.ts | 22 +- .../views/ai/bots/pages/setting/config.vue | 560 ++++++- .../src/views/ai/bots/pages/setting/index.vue | 13 +- .../src/views/config/settings/Settings.vue | 22 +- .../app/src/views/publicChat/index.vue | 1431 +++++++++++++++++ 12 files changed, 2152 insertions(+), 30 deletions(-) create mode 100644 easyflow-ui-admin/app/src/views/publicChat/index.vue diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysTempTokenController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysTempTokenController.java index e96b1bc..3fc6975 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysTempTokenController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysTempTokenController.java @@ -24,6 +24,8 @@ public class SysTempTokenController { LoginAccount loginAccount = new LoginAccount(); loginAccount.setId(BigInteger.valueOf(0)); loginAccount.setLoginName("匿名用户"); + loginAccount.setTenantId(BigInteger.ZERO); + loginAccount.setDeptId(BigInteger.ZERO); StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount); return Result.ok("", tokenValue); diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/listener/ChatStreamListener.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/listener/ChatStreamListener.java index 87d6653..f419b8b 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/listener/ChatStreamListener.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/listener/ChatStreamListener.java @@ -1,6 +1,7 @@ package tech.easyflow.ai.easyagents.listener; import com.easyagents.core.message.AiMessage; +import com.easyagents.core.message.ToolCall; import com.easyagents.core.message.ToolMessage; import com.easyagents.core.model.chat.ChatModel; import com.easyagents.core.model.chat.ChatOptions; @@ -66,11 +67,18 @@ public class ChatStreamListener implements StreamResponseListener { if (aiMessage.isFinalDelta() && aiMessageResponse.hasToolCalls()) { this.canStop = false; // 工具调用期间,禁止执行onStop this.hasToolCall = true; // 标记已进入过工具调用 + List toolCalls = aiMessage.getToolCalls(); + if (toolCalls != null) { + for (ToolCall toolCall : toolCalls) { + sendToolCallEnvelope(toolCall); + } + } aiMessage.setContent(null); memoryPrompt.addMessage(aiMessage); List toolMessages = aiMessageResponse.executeToolCallsAndGetToolMessages(); for (ToolMessage toolMessage : toolMessages) { memoryPrompt.addMessage(toolMessage); + sendToolResultEnvelope(toolMessage); } chatModel.chatStream(memoryPrompt, this, chatOptions); } else { @@ -151,6 +159,43 @@ public class ChatStreamListener implements StreamResponseListener { } } + private void sendToolCallEnvelope(ToolCall toolCall) { + if (toolCall == null) { + return; + } + ChatEnvelope> chatEnvelope = new ChatEnvelope<>(); + chatEnvelope.setDomain(ChatDomain.TOOL); + chatEnvelope.setType(ChatType.TOOL_CALL); + + Map payload = new LinkedHashMap<>(); + payload.put("tool_call_id", toolCall.getId()); + payload.put("name", toolCall.getName()); + payload.put("arguments", toolCall.getArguments()); + chatEnvelope.setPayload(payload); + boolean sent = sseEmitter.send(chatEnvelope); + if (!sent) { + throw new IllegalStateException("SSE emitter has already completed while sending tool call envelope"); + } + } + + private void sendToolResultEnvelope(ToolMessage toolMessage) { + if (toolMessage == null) { + return; + } + ChatEnvelope> chatEnvelope = new ChatEnvelope<>(); + chatEnvelope.setDomain(ChatDomain.TOOL); + chatEnvelope.setType(ChatType.TOOL_RESULT); + + Map payload = new LinkedHashMap<>(); + payload.put("tool_call_id", toolMessage.getToolCallId()); + payload.put("result", toolMessage.getContent()); + chatEnvelope.setPayload(payload); + boolean sent = sseEmitter.send(chatEnvelope); + if (!sent) { + throw new IllegalStateException("SSE emitter has already completed while sending tool result envelope"); + } + } + public void sendSystemError(ChatSseEmitter sseEmitter, String message, Throwable throwable) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java index ba1c560..ff5cbb4 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java @@ -148,7 +148,16 @@ public class BotServiceImpl extends ServiceImpl implements BotSe return ChatSseUtil.sendSystemError(conversationId, "请配置大模型!"); } boolean login = StpUtil.isLogin(); - if (!login && !aiBot.isAnonymousEnabled()) { + boolean anonymousAccount = false; + if (login) { + try { + anonymousAccount = SaTokenUtil.getLoginAccount() != null + && BigInteger.ZERO.equals(SaTokenUtil.getLoginAccount().getId()); + } catch (Exception ignored) { + anonymousAccount = false; + } + } + if ((!login || anonymousAccount) && !aiBot.isAnonymousEnabled()) { return ChatSseUtil.sendSystemError(conversationId, "此聊天助手不支持匿名访问"); } Map modelOptions = aiBot.getModelOptions(); diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json index 36dae8b..b321963 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json @@ -12,8 +12,39 @@ "enableDeepThinking": "EnableDeepThinking", "publish": "Publish", "postToWeChatOfficialAccount": "PostToWeChatOfficialAccount", + "publishExternalLink": "Publish External Chat Link", "configured": "Configured", "notConfigured": "NotConfigured", + "chatPublishBaseUrlMissing": "Publish base URL is not configured. Please set it in system settings first.", + "chatExternalLink": "Chat External Link", + "iframeEmbedCode": "Iframe Embed Code", + "copyLink": "Copy Link", + "copyIframeCode": "Copy Code", + "openPublicPage": "Open Page", + "allowAnonymousAccess": "Allow Anonymous Access", + "embedUsage": "Embed Notes", + "embedUsageTip1": "If embedding fails, check whether gateway/Nginx sets X-Frame-Options or strict Content-Security-Policy.", + "embedUsageTip2": "Embed only in trusted sites and combine with rate limiting.", + "publicPageBlocked": "Anonymous access is disabled for this bot", + "publicPageBlockedTip": "Enable anonymous access in publish settings before using external links.", + "publicChatTitle": "Public Chat Assistant", + "publicChatSubtitle": "Powered by EasyFlow", + "publicChatPlaceholder": "Type your question and press Enter to send", + "publicChatInputHint": "Shift + Enter for newline", + "publicChatSend": "Send", + "publicChatStop": "Stop", + "publicChatLoading": "Initializing chat environment...", + "publicChatThinking": "Thinking...", + "publicChatInitError": "Initialization failed, please try again later", + "publicChatAssistantReply": "Assistant Reply", + "publicChatToolCalling": "Calling tool", + "publicChatToolDone": "Tool completed", + "publicChatToolUnknown": "Unnamed tool", + "publicChatToolExpand": "Expand", + "publicChatToolCollapse": "Collapse", + "publicChatCopySuccess": "Copied", + "publicChatCopyFail": "Copy failed", + "basicInfo": "Basic Info", "placeholder": { "welcome": "Please enter welcome message", "prompt": "You are an AI assistant. Please provide clear and accurate answers based on the user's questions.", @@ -23,5 +54,6 @@ "aiOptimization": "AI Optimization", "weChatOfficialAccountConfiguration": "WeChat Official Account configuration", "aiOptimizedPrompts": "AI Optimized Prompts", - "chatAssistant": "Chat Assistant" + "chatAssistant": "Chat Assistant", + "publicChatStopped": "Generation stopped" } diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json b/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json index 171e521..ebf6ea6 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json @@ -6,5 +6,7 @@ "basic": "BasicInformation", "updatePwd": "UpdatePassword", "systemAIFunctionSettings": "System AI Function Settings", - "note": "Note: This config only applies to system AI features, not [Chat Assistant]." + "note": "Note: This config only applies to system AI features, not [Chat Assistant].", + "chatPublishBaseUrl": "Chat Publish Base URL", + "chatPublishBaseUrlPlaceholder": "For example: https://your-admin-domain" } diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json index 58e2bb5..0866280 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json @@ -12,8 +12,39 @@ "enableDeepThinking": "是否启用深度思考", "publish": "发布", "postToWeChatOfficialAccount": "发布到微信公众号", + "publishExternalLink": "发布外链聊天页", "configured": "已配置", "notConfigured": "未配置", + "chatPublishBaseUrlMissing": "未配置发布域名,请先到系统设置中配置", + "chatExternalLink": "聊天外链", + "iframeEmbedCode": "iframe 嵌入代码", + "copyLink": "复制链接", + "copyIframeCode": "复制代码", + "openPublicPage": "打开页面", + "allowAnonymousAccess": "允许匿名访问", + "embedUsage": "嵌入说明", + "embedUsageTip1": "若无法嵌入,请检查网关/Nginx 是否设置 X-Frame-Options 或严格 Content-Security-Policy。", + "embedUsageTip2": "建议仅在可信站点嵌入,并结合限流策略使用。", + "publicPageBlocked": "该助手未开放匿名访问", + "publicPageBlockedTip": "请在发布设置中开启“允许匿名访问”后再通过外链访问。", + "publicChatTitle": "公开聊天助手", + "publicChatSubtitle": "由 EasyFlow 驱动", + "publicChatPlaceholder": "输入你的问题,按 Enter 发送", + "publicChatInputHint": "Shift + Enter 换行", + "publicChatSend": "发送", + "publicChatStop": "停止", + "publicChatLoading": "正在初始化聊天环境...", + "publicChatThinking": "思考中...", + "publicChatInitError": "初始化失败,请稍后重试", + "publicChatAssistantReply": "助手回复", + "publicChatToolCalling": "工具调用中", + "publicChatToolDone": "工具已返回", + "publicChatToolUnknown": "未命名工具", + "publicChatToolExpand": "展开", + "publicChatToolCollapse": "收起", + "publicChatCopySuccess": "复制成功", + "publicChatCopyFail": "复制失败", + "basicInfo": "基础信息", "placeholder": { "welcome": "请输入欢迎语", "prompt": "你是一个AI助手,请根据用户的问题给出清晰、准确的回答。", @@ -23,5 +54,6 @@ "aiOptimization": "AI优化", "weChatOfficialAccountConfiguration": "微信公众号配置", "aiOptimizedPrompts": "AI优化提示词", - "chatAssistant": "聊天助手" + "chatAssistant": "聊天助手", + "publicChatStopped": "已停止输出" } diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json index b9ec6c4..a75075b 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json @@ -6,5 +6,7 @@ "basic": "基本设置", "updatePwd": "修改密码", "systemAIFunctionSettings": "系统 AI 功能设置", - "note": "注意:此项配置,仅用于系统的 AI 功能,而非【聊天助手】。" + "note": "注意:此项配置,仅用于系统的 AI 功能,而非【聊天助手】。", + "chatPublishBaseUrl": "聊天外链发布域名", + "chatPublishBaseUrlPlaceholder": "例如:https://your-admin-domain" } diff --git a/easyflow-ui-admin/app/src/router/routes/core.ts b/easyflow-ui-admin/app/src/router/routes/core.ts index ad5bde9..549dda9 100644 --- a/easyflow-ui-admin/app/src/router/routes/core.ts +++ b/easyflow-ui-admin/app/src/router/routes/core.ts @@ -1,9 +1,9 @@ -import type { RouteRecordRaw } from 'vue-router'; +import type {RouteRecordRaw} from 'vue-router'; -import { LOGIN_PATH } from '@easyflow/constants'; -import { preferences } from '@easyflow/preferences'; +import {LOGIN_PATH} from '@easyflow/constants'; +import {preferences} from '@easyflow/preferences'; -import { $t } from '#/locales'; +import {$t} from '#/locales'; const BasicLayout = () => import('#/layouts/basic.vue'); const AuthPageLayout = () => import('#/layouts/auth.vue'); @@ -33,6 +33,20 @@ const coreRoutes: RouteRecordRaw[] = [ name: 'OAuth', path: '/oauth', }, + { + name: 'PublicBotChat', + path: '/embed/chat/:botId', + component: () => import('#/views/publicChat/index.vue'), + meta: { + title: 'PublicChat', + noBasicLayout: true, + hideInMenu: true, + hideInTab: true, + hideInBreadcrumb: true, + ignoreAccess: true, + loaded: true, + }, + }, /** * 根路由 * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。 diff --git a/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue b/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue index 6e87a5d..7fc2611 100644 --- a/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue +++ b/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue @@ -1,15 +1,16 @@