feat: Bot发布增加外链聊天界面

This commit is contained in:
2026-03-03 16:36:27 +08:00
parent 29f82ed1f0
commit 80409259c3
12 changed files with 2152 additions and 30 deletions

View File

@@ -24,6 +24,8 @@ public class SysTempTokenController {
LoginAccount loginAccount = new LoginAccount(); LoginAccount loginAccount = new LoginAccount();
loginAccount.setId(BigInteger.valueOf(0)); loginAccount.setId(BigInteger.valueOf(0));
loginAccount.setLoginName("匿名用户"); loginAccount.setLoginName("匿名用户");
loginAccount.setTenantId(BigInteger.ZERO);
loginAccount.setDeptId(BigInteger.ZERO);
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount); StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
return Result.ok("", tokenValue); return Result.ok("", tokenValue);

View File

@@ -1,6 +1,7 @@
package tech.easyflow.ai.easyagents.listener; package tech.easyflow.ai.easyagents.listener;
import com.easyagents.core.message.AiMessage; import com.easyagents.core.message.AiMessage;
import com.easyagents.core.message.ToolCall;
import com.easyagents.core.message.ToolMessage; import com.easyagents.core.message.ToolMessage;
import com.easyagents.core.model.chat.ChatModel; import com.easyagents.core.model.chat.ChatModel;
import com.easyagents.core.model.chat.ChatOptions; import com.easyagents.core.model.chat.ChatOptions;
@@ -66,11 +67,18 @@ public class ChatStreamListener implements StreamResponseListener {
if (aiMessage.isFinalDelta() && aiMessageResponse.hasToolCalls()) { if (aiMessage.isFinalDelta() && aiMessageResponse.hasToolCalls()) {
this.canStop = false; // 工具调用期间禁止执行onStop this.canStop = false; // 工具调用期间禁止执行onStop
this.hasToolCall = true; // 标记已进入过工具调用 this.hasToolCall = true; // 标记已进入过工具调用
List<ToolCall> toolCalls = aiMessage.getToolCalls();
if (toolCalls != null) {
for (ToolCall toolCall : toolCalls) {
sendToolCallEnvelope(toolCall);
}
}
aiMessage.setContent(null); aiMessage.setContent(null);
memoryPrompt.addMessage(aiMessage); memoryPrompt.addMessage(aiMessage);
List<ToolMessage> toolMessages = aiMessageResponse.executeToolCallsAndGetToolMessages(); List<ToolMessage> toolMessages = aiMessageResponse.executeToolCallsAndGetToolMessages();
for (ToolMessage toolMessage : toolMessages) { for (ToolMessage toolMessage : toolMessages) {
memoryPrompt.addMessage(toolMessage); memoryPrompt.addMessage(toolMessage);
sendToolResultEnvelope(toolMessage);
} }
chatModel.chatStream(memoryPrompt, this, chatOptions); chatModel.chatStream(memoryPrompt, this, chatOptions);
} else { } else {
@@ -151,6 +159,43 @@ public class ChatStreamListener implements StreamResponseListener {
} }
} }
private void sendToolCallEnvelope(ToolCall toolCall) {
if (toolCall == null) {
return;
}
ChatEnvelope<Map<String, Object>> chatEnvelope = new ChatEnvelope<>();
chatEnvelope.setDomain(ChatDomain.TOOL);
chatEnvelope.setType(ChatType.TOOL_CALL);
Map<String, Object> 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<Map<String, Object>> chatEnvelope = new ChatEnvelope<>();
chatEnvelope.setDomain(ChatDomain.TOOL);
chatEnvelope.setType(ChatType.TOOL_RESULT);
Map<String, Object> 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, public void sendSystemError(ChatSseEmitter sseEmitter,
String message, String message,
Throwable throwable) { Throwable throwable) {

View File

@@ -148,7 +148,16 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
return ChatSseUtil.sendSystemError(conversationId, "请配置大模型!"); return ChatSseUtil.sendSystemError(conversationId, "请配置大模型!");
} }
boolean login = StpUtil.isLogin(); 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, "此聊天助手不支持匿名访问"); return ChatSseUtil.sendSystemError(conversationId, "此聊天助手不支持匿名访问");
} }
Map<String, Object> modelOptions = aiBot.getModelOptions(); Map<String, Object> modelOptions = aiBot.getModelOptions();

View File

@@ -12,8 +12,39 @@
"enableDeepThinking": "EnableDeepThinking", "enableDeepThinking": "EnableDeepThinking",
"publish": "Publish", "publish": "Publish",
"postToWeChatOfficialAccount": "PostToWeChatOfficialAccount", "postToWeChatOfficialAccount": "PostToWeChatOfficialAccount",
"publishExternalLink": "Publish External Chat Link",
"configured": "Configured", "configured": "Configured",
"notConfigured": "NotConfigured", "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": { "placeholder": {
"welcome": "Please enter welcome message", "welcome": "Please enter welcome message",
"prompt": "You are an AI assistant. Please provide clear and accurate answers based on the user's questions.", "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", "aiOptimization": "AI Optimization",
"weChatOfficialAccountConfiguration": "WeChat Official Account configuration", "weChatOfficialAccountConfiguration": "WeChat Official Account configuration",
"aiOptimizedPrompts": "AI Optimized Prompts", "aiOptimizedPrompts": "AI Optimized Prompts",
"chatAssistant": "Chat Assistant" "chatAssistant": "Chat Assistant",
"publicChatStopped": "Generation stopped"
} }

View File

@@ -6,5 +6,7 @@
"basic": "BasicInformation", "basic": "BasicInformation",
"updatePwd": "UpdatePassword", "updatePwd": "UpdatePassword",
"systemAIFunctionSettings": "System AI Function Settings", "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"
} }

View File

@@ -12,8 +12,39 @@
"enableDeepThinking": "是否启用深度思考", "enableDeepThinking": "是否启用深度思考",
"publish": "发布", "publish": "发布",
"postToWeChatOfficialAccount": "发布到微信公众号", "postToWeChatOfficialAccount": "发布到微信公众号",
"publishExternalLink": "发布外链聊天页",
"configured": "已配置", "configured": "已配置",
"notConfigured": "未配置", "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": { "placeholder": {
"welcome": "请输入欢迎语", "welcome": "请输入欢迎语",
"prompt": "你是一个AI助手请根据用户的问题给出清晰、准确的回答。", "prompt": "你是一个AI助手请根据用户的问题给出清晰、准确的回答。",
@@ -23,5 +54,6 @@
"aiOptimization": "AI优化", "aiOptimization": "AI优化",
"weChatOfficialAccountConfiguration": "微信公众号配置", "weChatOfficialAccountConfiguration": "微信公众号配置",
"aiOptimizedPrompts": "AI优化提示词", "aiOptimizedPrompts": "AI优化提示词",
"chatAssistant": "聊天助手" "chatAssistant": "聊天助手",
"publicChatStopped": "已停止输出"
} }

View File

@@ -6,5 +6,7 @@
"basic": "基本设置", "basic": "基本设置",
"updatePwd": "修改密码", "updatePwd": "修改密码",
"systemAIFunctionSettings": "系统 AI 功能设置", "systemAIFunctionSettings": "系统 AI 功能设置",
"note": "注意:此项配置,仅用于系统的 AI 功能,而非【聊天助手】。" "note": "注意:此项配置,仅用于系统的 AI 功能,而非【聊天助手】。",
"chatPublishBaseUrl": "聊天外链发布域名",
"chatPublishBaseUrlPlaceholder": "例如https://your-admin-domain"
} }

View File

@@ -33,6 +33,20 @@ const coreRoutes: RouteRecordRaw[] = [
name: 'OAuth', name: 'OAuth',
path: '/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。 * 使用基础布局作为所有页面的父级容器子级就不必配置BasicLayout。

View File

@@ -1,15 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import type {AiLlm, BotInfo} from '@easyflow/types'; import type {AiLlm, BotInfo} from '@easyflow/types';
import { onMounted, ref, watch } from 'vue'; import {computed, onMounted, ref, watch} from 'vue';
import {useRoute} from 'vue-router'; import {useRoute} from 'vue-router';
import {$t} from '@easyflow/locales'; import {$t} from '@easyflow/locales';
import {useBotStore} from '@easyflow/stores'; import {useBotStore} from '@easyflow/stores';
import { Delete, Plus, Setting } from '@element-plus/icons-vue'; import {CopyDocument, Delete, InfoFilled, Link, Plus, Setting} from '@element-plus/icons-vue';
import {useDebounceFn} from '@vueuse/core'; import {useDebounceFn} from '@vueuse/core';
import { import {
ElAlert,
ElButton, ElButton,
ElCol, ElCol,
ElCollapse, ElCollapse,
@@ -20,13 +21,16 @@ import {
ElMessage, ElMessage,
ElRow, ElRow,
ElSelect, ElSelect,
ElSkeleton,
ElSlider, ElSlider,
ElSwitch, ElSwitch,
ElTooltip,
} from 'element-plus'; } from 'element-plus';
import {tryit} from 'radash'; import {tryit} from 'radash';
import { import {
getPerQuestions, getPerQuestions,
updateBotApi,
updateBotOptions, updateBotOptions,
updateLlmId, updateLlmId,
updateLlmOptions, updateLlmOptions,
@@ -36,6 +40,8 @@ import ProblemPresupposition from '#/components/chat/ProblemPresupposition.vue';
import PublishWxOfficalAccount from '#/components/chat/PublishWxOfficalAccount.vue'; import PublishWxOfficalAccount from '#/components/chat/PublishWxOfficalAccount.vue';
import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue'; import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue';
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue'; import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
import DictSelect from '#/components/dict/DictSelect.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
interface SelectedMcpTool { interface SelectedMcpTool {
name: string; name: string;
@@ -60,6 +66,89 @@ const llmConfig = ref({
const dialogueSettings = ref({ const dialogueSettings = ref({
welcomeMessage: '', welcomeMessage: '',
enableDeepThinking: false, enableDeepThinking: false,
anonymousEnabled: false,
});
const publishBaseUrl = ref('');
const routerHistoryMode = import.meta.env.VITE_ROUTER_HISTORY;
const normalizePublishBaseUrl = (value: string) => {
const raw = value.trim();
if (!raw) {
return '';
}
if (/^https?:\/\//i.test(raw)) {
return raw.replace(/\/+$/, '');
}
if (raw.startsWith('//')) {
return `${window.location.protocol}${raw}`.replace(/\/+$/, '');
}
// 支持仅填写 localhost 这类主机名,自动补协议和当前端口,避免被当作相对路径。
const firstSegment = raw.split('/')[0] || '';
const hasPort = /:\d+$/.test(firstSegment);
let hostAndPath = raw;
if (
!hasPort &&
firstSegment === window.location.hostname &&
window.location.port
) {
hostAndPath = `${firstSegment}:${window.location.port}${raw.slice(firstSegment.length)}`;
}
return `${window.location.protocol}//${hostAndPath}`.replace(/\/+$/, '');
};
const hasPublishBaseUrl = computed(
() => publishBaseUrl.value.trim().length > 0,
);
const publicChatPath = computed(() =>
routerHistoryMode === 'hash'
? `/#/embed/chat/${botId.value}`
: `/embed/chat/${botId.value}`,
);
const publicChatUrl = computed(() => {
if (!hasPublishBaseUrl.value) {
return '';
}
const base = normalizePublishBaseUrl(publishBaseUrl.value);
return `${base}${publicChatPath.value}`;
});
const publicChatEmbedUrl = computed(() => {
if (!publicChatUrl.value) {
return '';
}
return `${publicChatUrl.value}?embed=1`;
});
const iframeCode = computed(() => {
if (!publicChatEmbedUrl.value) {
return '';
}
return `<iframe
src="${publicChatEmbedUrl.value}"
width="100%"
height="720"
style="border:0;border-radius:12px;overflow:hidden"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
></iframe>`;
});
const escapeHtml = (value: string) =>
value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
const iframeCodeHighlighted = computed(() => {
if (!iframeCode.value) {
return '';
}
const escaped = escapeHtml(iframeCode.value);
return escaped
.replace(
/(&lt;\/?)([a-zA-Z][\w-]*)/g,
'$1<span class="hljs-name">$2</span>',
)
.replace(
/([:@a-zA-Z_][\w:-]*)=(&quot;.*?&quot;)/g,
'<span class="hljs-attr">$1</span>=<span class="hljs-string">$2</span>',
);
}); });
watch( watch(
props, props,
@@ -135,6 +224,20 @@ const getAiBotWorkflowList = async () => {
}); });
}; };
const botInfo = ref<BotInfo>(); const botInfo = ref<BotInfo>();
const basicInfoForm = ref({
title: '',
alias: '',
categoryId: '',
});
const updatingBotIcon = ref(false);
const updatingBasicInfo = ref(false);
const syncingBasicInfoForm = ref(false);
const getPublishBaseUrl = async () => {
const [, res] = await tryit(api.get)('/api/v1/sysOption/list?keys=chat_publish_base_url');
if (res?.errorCode === 0) {
publishBaseUrl.value = (res.data?.chat_publish_base_url || '').trim();
}
};
const getBotDetail = async () => { const getBotDetail = async () => {
api api
.get('/api/v1/bot/detail', { .get('/api/v1/bot/detail', {
@@ -146,11 +249,21 @@ const getBotDetail = async () => {
if (res.errorCode === 0) { if (res.errorCode === 0) {
botInfo.value = res.data; botInfo.value = res.data;
if (res.data.options) { if (res.data.options) {
dialogueSettings.value = res.data.options; dialogueSettings.value = {
...dialogueSettings.value,
...res.data.options,
};
} }
if (res.data.options?.presetQuestions) { if (res.data.options?.presetQuestions) {
botStore.setPresetQuestions(res.data?.options?.presetQuestions); botStore.setPresetQuestions(res.data?.options?.presetQuestions);
} }
syncingBasicInfoForm.value = true;
basicInfoForm.value = {
title: res.data.title || '',
alias: res.data.alias || '',
categoryId: res.data.categoryId || '',
};
syncingBasicInfoForm.value = false;
} }
}); });
}; };
@@ -163,6 +276,7 @@ const getLlmListData = async () => {
}); });
}; };
onMounted(async () => { onMounted(async () => {
getPublishBaseUrl();
getAiBotPluginToolList(); getAiBotPluginToolList();
getAiBotKnowledgeList(); getAiBotKnowledgeList();
getAiBotWorkflowList(); getAiBotWorkflowList();
@@ -171,6 +285,33 @@ onMounted(async () => {
getLlmListData(); getLlmListData();
}); });
const handleAnonymousAccessChange = (
value: boolean | number | string,
) => {
handleDialogOptionsStrChange('anonymousEnabled', value);
};
const handleCopyValue = async (value: string, successMessage?: string) => {
if (!value) {
ElMessage.warning($t('bot.chatPublishBaseUrlMissing'));
return;
}
try {
await navigator.clipboard.writeText(value);
ElMessage.success(successMessage || $t('bot.publicChatCopySuccess'));
} catch {
ElMessage.error($t('bot.publicChatCopyFail'));
}
};
const openPublicPage = () => {
if (!publicChatUrl.value) {
ElMessage.warning($t('bot.chatPublishBaseUrlMissing'));
return;
}
window.open(publicChatUrl.value, '_blank', 'noopener,noreferrer');
};
const handleLlmChange = async (value: string) => { const handleLlmChange = async (value: string) => {
if (!props.bot) return; if (!props.bot) return;
@@ -440,10 +581,152 @@ const formatSelectedMcpData = () => {
return formattedData; return formattedData;
}; };
const persistBotBasicInfo = async (
patch: Partial<Pick<BotInfo, 'alias' | 'categoryId' | 'icon' | 'title'>>,
) => {
if (!botInfo.value || !props.hasSavePermission) {
return false;
}
const snapshot = { ...botInfo.value };
const next = {
...snapshot,
...patch,
};
botInfo.value = next;
const [, res] = await tryit(updateBotApi)({
id: String(next.id),
icon: next.icon || '',
title: next.title || '',
alias: next.alias || '',
description: next.description || '',
categoryId: next.categoryId,
status: next.status,
});
if (res?.errorCode === 0) {
return true;
}
botInfo.value = snapshot;
ElMessage.error(res?.message || $t('message.saveFailMessage'));
return false;
};
const handleBotIconUpdate = async (iconPath: string) => {
if (!iconPath || !botInfo.value) {
return;
}
if (!props.hasSavePermission) {
ElMessage.warning($t('bot.noPermission'));
return;
}
if (updatingBotIcon.value) {
return;
}
updatingBotIcon.value = true;
const ok = await persistBotBasicInfo({ icon: iconPath });
updatingBotIcon.value = false;
if (ok) {
ElMessage.success($t('message.updateOkMessage'));
}
};
const handleBasicInfoChange = async (
key: 'alias' | 'categoryId' | 'title',
value: any,
) => {
if (!botInfo.value || !props.hasSavePermission || syncingBasicInfoForm.value) {
return;
}
if (updatingBasicInfo.value) {
return;
}
const normalizedValue = key === 'categoryId' ? value : String(value || '').trim();
if ((key === 'title' || key === 'alias') && !normalizedValue) {
ElMessage.warning($t('message.required'));
basicInfoForm.value[key] = botInfo.value[key] as string;
return;
}
if (botInfo.value[key] === normalizedValue) {
basicInfoForm.value[key] = normalizedValue as any;
return;
}
updatingBasicInfo.value = true;
const ok = await persistBotBasicInfo({
[key]: normalizedValue,
} as Partial<Pick<BotInfo, 'alias' | 'categoryId' | 'title'>>);
updatingBasicInfo.value = false;
if (ok) {
ElMessage.success($t('message.updateOkMessage'));
basicInfoForm.value[key] = normalizedValue as any;
} else {
basicInfoForm.value[key] = botInfo.value[key] as any;
}
};
</script> </script>
<template> <template>
<div class="config-container flex flex-col gap-3"> <div class="config-container flex flex-col gap-3">
<!-- 基础信息 -->
<div
class="bg-background dark:border-border flex flex-col gap-3 rounded-lg p-3 dark:border"
>
<h1 class="text-base font-medium">{{ $t('bot.basicInfo') }}</h1>
<div class="bot-basic-info-container">
<div class="bot-basic-info-layout">
<div class="bot-avatar-panel">
<span class="bot-avatar-label">{{ $t('common.avatar') }}</span>
<div
:class="[
'bot-avatar-upload-wrap',
(!hasSavePermission || updatingBotIcon) ? 'is-disabled' : '',
]"
>
<UploadAvatar
v-if="botInfo"
v-model="botInfo.icon"
@success="handleBotIconUpdate"
/>
<ElSkeleton v-else animated :rows="1" style="width: 100px" />
</div>
</div>
<div class="bot-basic-form-panel">
<div class="bot-basic-form-item">
<span class="bot-basic-form-label">{{ $t('aiWorkflow.title') }}</span>
<ElInput
v-model="basicInfoForm.title"
:disabled="!hasSavePermission || updatingBasicInfo"
@change="(value) => handleBasicInfoChange('title', value)"
/>
</div>
<div class="bot-basic-form-item">
<span class="bot-basic-form-label">{{ $t('plugin.alias') }}</span>
<ElInput
v-model="basicInfoForm.alias"
:disabled="!hasSavePermission || updatingBasicInfo"
@change="(value) => handleBasicInfoChange('alias', value)"
/>
</div>
<div class="bot-basic-form-item">
<span class="bot-basic-form-label">{{ $t('aiWorkflow.categoryId') }}</span>
<DictSelect
v-model="basicInfoForm.categoryId"
dict-code="aiBotCategory"
:disabled="!hasSavePermission || updatingBasicInfo"
@change="(value: any) => handleBasicInfoChange('categoryId', value)"
/>
</div>
</div>
</div>
</div>
</div>
<!-- 大模型 --> <!-- 大模型 -->
<div <div
class="bg-background dark:border-border flex flex-col gap-3 rounded-lg p-3 dark:border" class="bg-background dark:border-border flex flex-col gap-3 rounded-lg p-3 dark:border"
@@ -822,6 +1105,87 @@ const formatSelectedMcpData = () => {
</div> </div>
</div> </div>
</ElCollapseItem> </ElCollapseItem>
<ElCollapseItem :title="$t('bot.publishExternalLink')">
<div class="publish-external-container">
<div class="publish-external-top">
<span>{{ $t('bot.allowAnonymousAccess') }}</span>
<ElSwitch
v-model="dialogueSettings.anonymousEnabled"
@change="handleAnonymousAccessChange"
/>
</div>
<div v-if="!hasPublishBaseUrl" class="publish-base-url-warning">
<ElAlert
:title="$t('bot.chatPublishBaseUrlMissing')"
type="warning"
:closable="false"
/>
</div>
<div class="publish-external-item">
<label class="publish-external-label">
{{ $t('bot.chatExternalLink') }}
</label>
<ElInput :model-value="publicChatUrl" readonly />
<div class="publish-external-actions">
<ElButton
size="small"
type="primary"
plain
@click="handleCopyValue(publicChatUrl)"
>
<ElIcon class="mr-1">
<CopyDocument />
</ElIcon>
{{ $t('bot.copyLink') }}
</ElButton>
<ElButton
size="small"
type="primary"
@click="openPublicPage"
>
<ElIcon class="mr-1">
<Link />
</ElIcon>
{{ $t('bot.openPublicPage') }}
</ElButton>
</div>
</div>
<div class="publish-external-item">
<label class="publish-external-label flex items-center gap-1">
<span>{{ $t('bot.iframeEmbedCode') }}</span>
<ElTooltip
effect="dark"
placement="top"
>
<template #content>
<div>{{ $t('bot.embedUsageTip1') }}</div>
<div class="mt-1">{{ $t('bot.embedUsageTip2') }}</div>
</template>
<ElIcon class="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors"><InfoFilled /></ElIcon>
</ElTooltip>
</label>
<div class="publish-external-code-preview">
<div class="publish-external-code-topbar">HTML</div>
<pre class="publish-external-code-block"><code
class="hljs language-xml"
v-html="iframeCodeHighlighted"
></code></pre>
</div>
<div class="publish-external-actions">
<ElButton
size="small"
plain
@click="handleCopyValue(iframeCode)"
>
<ElIcon class="mr-1">
<CopyDocument />
</ElIcon>
{{ $t('bot.copyIframeCode') }}
</ElButton>
</div>
</div>
</div>
</ElCollapseItem>
</ElCollapse> </ElCollapse>
</div> </div>
</div> </div>
@@ -1002,6 +1366,107 @@ const formatSelectedMcpData = () => {
justify-content: flex-end; justify-content: flex-end;
} }
.publish-external-container {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
background-color: var(--bot-collapse-itme-back);
}
.publish-external-top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: var(--el-bg-color);
border-radius: 8px;
}
.publish-base-url-warning {
margin-bottom: 2px;
}
.publish-external-item {
display: flex;
min-width: 0;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--el-bg-color);
border-radius: 8px;
}
.publish-external-label {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-regular);
}
.publish-external-actions {
display: flex;
justify-content: flex-end;
}
.publish-external-alert {
margin-top: 8px;
}
.publish-external-code-preview {
max-width: 100%;
overflow: hidden;
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 10px;
}
.publish-external-code-topbar {
padding: 8px 12px;
font-size: 12px;
font-weight: 600;
line-height: 1.2;
color: #93c5fd;
background: #111b31;
border-bottom: 1px solid #1f2d49;
}
.publish-external-code-block {
width: 100%;
max-width: 100%;
max-height: 180px;
margin: 0;
overflow-x: auto;
overflow-y: auto;
padding: 10px 12px;
}
.publish-external-code-block code {
display: block;
min-width: 0;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #d8e1f0;
}
.publish-external-code-block code .hljs-tag,
.publish-external-code-block code .hljs-name,
.publish-external-code-block code .hljs-keyword {
color: #93c5fd;
}
.publish-external-code-block code .hljs-attr {
color: #fcd34d;
}
.publish-external-code-block code .hljs-string {
color: #86efac;
}
:deep(.el-collapse) { :deep(.el-collapse) {
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
@@ -1053,4 +1518,79 @@ const formatSelectedMcpData = () => {
padding: 10px; padding: 10px;
background-color: var(--bot-collapse-itme-back); background-color: var(--bot-collapse-itme-back);
} }
.bot-basic-info-container {
padding: 16px 20px;
background-color: var(--bot-collapse-itme-back);
border-radius: 8px;
}
.bot-basic-info-layout {
display: flex;
flex-direction: column;
gap: 20px;
align-items: stretch;
}
.bot-avatar-panel {
display: flex;
flex-direction: row;
align-items: center;
}
.bot-basic-form-panel {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
.bot-basic-form-item {
display: flex;
flex-direction: row;
align-items: center;
}
.bot-avatar-label,
.bot-basic-form-label {
flex: 0 0 60px;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
text-align: left;
padding-right: 16px;
}
.bot-avatar-upload-wrap {
flex: 1;
}
.bot-basic-form-item > *:not(.bot-basic-form-label) {
flex: 1;
}
.bot-avatar-upload-wrap.is-disabled {
pointer-events: none;
opacity: 0.6;
}
.bot-avatar-upload-wrap :deep(.avatar-uploader .avatar) {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
}
.bot-avatar-upload-wrap :deep(.el-icon.avatar-uploader-icon) {
width: 50px;
height: 50px;
font-size: 18px;
border-radius: 50%;
}
.bot-avatar-upload-wrap :deep(.avatar-uploader .el-upload) {
width: 50px;
height: 50px;
border-radius: 50%;
}
</style> </style>

View File

@@ -62,5 +62,6 @@ const fetchBotDetail = async (id: string) => {
.row-item { .row-item {
height: 100%; height: 100%;
flex: 1; flex: 1;
min-width: 0;
} }
</style> </style>

View File

@@ -45,6 +45,7 @@ interface SettingsEntity {
chatgpt_chatPath: string; chatgpt_chatPath: string;
chatgpt_endpoint: string; chatgpt_endpoint: string;
chatgpt_model_name: string; chatgpt_model_name: string;
chat_publish_base_url: string;
model_of_chat: string; model_of_chat: string;
} }
@@ -64,11 +65,14 @@ function getBrands() {
function getOptions() { function getOptions() {
api api
.get( .get(
'/api/v1/sysOption/list?keys=model_of_chat&keys=chatgpt_endpoint&keys=chatgpt_chatPath&keys=chatgpt_api_key&keys=chatgpt_model_name', '/api/v1/sysOption/list?keys=model_of_chat&keys=chatgpt_endpoint&keys=chatgpt_chatPath&keys=chatgpt_api_key&keys=chatgpt_model_name&keys=chat_publish_base_url',
) )
.then((res) => { .then((res) => {
if (res.errorCode === 0) { if (res.errorCode === 0) {
entity.value = res.data; entity.value = {
...entity.value,
...res.data,
};
} }
}); });
} }
@@ -83,6 +87,7 @@ const entity = ref<SettingsEntity>({
chatgpt_chatPath: '', chatgpt_chatPath: '',
chatgpt_endpoint: '', chatgpt_endpoint: '',
chatgpt_model_name: '', chatgpt_model_name: '',
chat_publish_base_url: '',
}); });
function formatLlmList(data: ModelProvider[]): LlmOption[] { function formatLlmList(data: ModelProvider[]): LlmOption[] {
@@ -150,6 +155,13 @@ function handleSave() {
<ElFormItem label="ApiKey"> <ElFormItem label="ApiKey">
<ElInput v-model="entity.chatgpt_api_key" clearable /> <ElInput v-model="entity.chatgpt_api_key" clearable />
</ElFormItem> </ElFormItem>
<ElFormItem :label="$t('settingsConfig.chatPublishBaseUrl')">
<ElInput
v-model="entity.chat_publish_base_url"
clearable
:placeholder="$t('settingsConfig.chatPublishBaseUrlPlaceholder')"
/>
</ElFormItem>
</ElForm> </ElForm>
<div class="settings-button-container"> <div class="settings-button-container">
<ElButton type="primary" @click="handleSave"> <ElButton type="primary" @click="handleSave">

File diff suppressed because it is too large Load Diff