feat: Bot发布增加外链聊天界面
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "已停止输出"
|
||||
}
|
||||
|
||||
@@ -6,5 +6,7 @@
|
||||
"basic": "基本设置",
|
||||
"updatePwd": "修改密码",
|
||||
"systemAIFunctionSettings": "系统 AI 功能设置",
|
||||
"note": "注意:此项配置,仅用于系统的 AI 功能,而非【聊天助手】。"
|
||||
"note": "注意:此项配置,仅用于系统的 AI 功能,而非【聊天助手】。",
|
||||
"chatPublishBaseUrl": "聊天外链发布域名",
|
||||
"chatPublishBaseUrlPlaceholder": "例如:https://your-admin-domain"
|
||||
}
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
const iframeCodeHighlighted = computed(() => {
|
||||
if (!iframeCode.value) {
|
||||
return '';
|
||||
}
|
||||
const escaped = escapeHtml(iframeCode.value);
|
||||
return escaped
|
||||
.replace(
|
||||
/(<\/?)([a-zA-Z][\w-]*)/g,
|
||||
'$1<span class="hljs-name">$2</span>',
|
||||
)
|
||||
.replace(
|
||||
/([:@a-zA-Z_][\w:-]*)=(".*?")/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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
1431
easyflow-ui-admin/app/src/views/publicChat/index.vue
Normal file
1431
easyflow-ui-admin/app/src/views/publicChat/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user