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.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);

View File

@@ -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<ToolCall> toolCalls = aiMessage.getToolCalls();
if (toolCalls != null) {
for (ToolCall toolCall : toolCalls) {
sendToolCallEnvelope(toolCall);
}
}
aiMessage.setContent(null);
memoryPrompt.addMessage(aiMessage);
List<ToolMessage> 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<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,
String message,
Throwable throwable) {

View File

@@ -148,7 +148,16 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> 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<String, Object> modelOptions = aiBot.getModelOptions();

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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": "已停止输出"
}

View File

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

View File

@@ -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。

View File

@@ -1,15 +1,16 @@
<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 { useRoute } from 'vue-router';
import {computed, onMounted, ref, watch} from 'vue';
import {useRoute} from 'vue-router';
import { $t } from '@easyflow/locales';
import { useBotStore } from '@easyflow/stores';
import {$t} from '@easyflow/locales';
import {useBotStore} from '@easyflow/stores';
import { Delete, Plus, Setting } from '@element-plus/icons-vue';
import { useDebounceFn } from '@vueuse/core';
import {CopyDocument, Delete, InfoFilled, Link, Plus, Setting} from '@element-plus/icons-vue';
import {useDebounceFn} from '@vueuse/core';
import {
ElAlert,
ElButton,
ElCol,
ElCollapse,
@@ -20,22 +21,27 @@ import {
ElMessage,
ElRow,
ElSelect,
ElSkeleton,
ElSlider,
ElSwitch,
ElTooltip,
} from 'element-plus';
import { tryit } from 'radash';
import {tryit} from 'radash';
import {
getPerQuestions,
updateBotApi,
updateBotOptions,
updateLlmId,
updateLlmOptions,
} from '#/api';
import { api } from '#/api/request';
import {api} from '#/api/request';
import ProblemPresupposition from '#/components/chat/ProblemPresupposition.vue';
import PublishWxOfficalAccount from '#/components/chat/PublishWxOfficalAccount.vue';
import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue';
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
import DictSelect from '#/components/dict/DictSelect.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
interface SelectedMcpTool {
name: string;
@@ -60,6 +66,89 @@ const llmConfig = ref({
const dialogueSettings = ref({
welcomeMessage: '',
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(
props,
@@ -135,6 +224,20 @@ const getAiBotWorkflowList = async () => {
});
};
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 () => {
api
.get('/api/v1/bot/detail', {
@@ -146,11 +249,21 @@ const getBotDetail = async () => {
if (res.errorCode === 0) {
botInfo.value = res.data;
if (res.data.options) {
dialogueSettings.value = res.data.options;
dialogueSettings.value = {
...dialogueSettings.value,
...res.data.options,
};
}
if (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 () => {
getPublishBaseUrl();
getAiBotPluginToolList();
getAiBotKnowledgeList();
getAiBotWorkflowList();
@@ -171,6 +285,33 @@ onMounted(async () => {
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) => {
if (!props.bot) return;
@@ -440,10 +581,152 @@ const formatSelectedMcpData = () => {
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>
<template>
<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
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>
</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>
</div>
</div>
@@ -1002,6 +1366,107 @@ const formatSelectedMcpData = () => {
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) {
box-sizing: border-box;
height: 100%;
@@ -1053,4 +1518,79 @@ const formatSelectedMcpData = () => {
padding: 10px;
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>

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { BotInfo } from '@easyflow/types';
import type {BotInfo} from '@easyflow/types';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import {computed, onMounted, ref} from 'vue';
import {useRoute} from 'vue-router';
import { tryit } from 'radash';
import {tryit} from 'radash';
import { getBotDetails } from '#/api';
import { hasPermission } from '#/api/common/hasPermission';
import {getBotDetails} from '#/api';
import {hasPermission} from '#/api/common/hasPermission';
import Config from './config.vue';
import Preview from './preview.vue';
@@ -62,5 +62,6 @@ const fetchBotDetail = async (id: string) => {
.row-item {
height: 100%;
flex: 1;
min-width: 0;
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import {onMounted, ref} from 'vue';
import { $t } from '@easyflow/locales';
import {$t} from '@easyflow/locales';
import {
ElAlert,
@@ -14,7 +14,7 @@ import {
ElSelect,
} from 'element-plus';
import { api } from '#/api/request.js';
import {api} from '#/api/request.js';
import providerList from '#/views/ai/model/modelUtils/providerList.json';
interface ProviderOptionExtra {
@@ -45,6 +45,7 @@ interface SettingsEntity {
chatgpt_chatPath: string;
chatgpt_endpoint: string;
chatgpt_model_name: string;
chat_publish_base_url: string;
model_of_chat: string;
}
@@ -64,11 +65,14 @@ function getBrands() {
function getOptions() {
api
.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) => {
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_endpoint: '',
chatgpt_model_name: '',
chat_publish_base_url: '',
});
function formatLlmList(data: ModelProvider[]): LlmOption[] {
@@ -150,6 +155,13 @@ function handleSave() {
<ElFormItem label="ApiKey">
<ElInput v-model="entity.chatgpt_api_key" clearable />
</ElFormItem>
<ElFormItem :label="$t('settingsConfig.chatPublishBaseUrl')">
<ElInput
v-model="entity.chat_publish_base_url"
clearable
:placeholder="$t('settingsConfig.chatPublishBaseUrlPlaceholder')"
/>
</ElFormItem>
</ElForm>
<div class="settings-button-container">
<ElButton type="primary" @click="handleSave">

File diff suppressed because it is too large Load Diff