feat: 接入聊天历史界面与外链会话恢复
- 新增管理端与用户端聊天历史接口和页面 - 外链聊天支持访问令牌登录、身份保活与当前会话恢复 - 聊天执行链路切到统一 runtime 与 chatlog 查询接口
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type {AiLlm, BotInfo} from '@easyflow/types';
|
||||
import type { AiLlm, BotInfo } from '@easyflow/types';
|
||||
|
||||
import {computed, 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 {CopyDocument, Delete, InfoFilled, Link, 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,
|
||||
@@ -19,6 +26,7 @@ import {
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElSkeleton,
|
||||
@@ -26,7 +34,7 @@ import {
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
import {tryit} from 'radash';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import {
|
||||
getPerQuestions,
|
||||
@@ -35,7 +43,7 @@ import {
|
||||
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';
|
||||
@@ -47,6 +55,14 @@ interface SelectedMcpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ApiKeyOption {
|
||||
id: string;
|
||||
apiKey: string;
|
||||
expiredAt?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
bot?: BotInfo;
|
||||
hasSavePermission?: boolean;
|
||||
@@ -68,6 +84,8 @@ const dialogueSettings = ref({
|
||||
enableDeepThinking: false,
|
||||
anonymousEnabled: false,
|
||||
});
|
||||
const selectedPublishApiKey = ref('');
|
||||
const publishApiKeyOptions = ref<ApiKeyOption[]>([]);
|
||||
const publishBaseUrl = ref('');
|
||||
const routerHistoryMode = import.meta.env.VITE_ROUTER_HISTORY;
|
||||
const normalizePublishBaseUrl = (value: string) => {
|
||||
@@ -102,18 +120,29 @@ const publicChatPath = computed(() =>
|
||||
? `/#/embed/chat/${botId.value}`
|
||||
: `/embed/chat/${botId.value}`,
|
||||
);
|
||||
const publicChatUrl = computed(() => {
|
||||
const buildPublicChatUrl = (embed = false) => {
|
||||
if (!hasPublishBaseUrl.value) {
|
||||
return '';
|
||||
}
|
||||
const base = normalizePublishBaseUrl(publishBaseUrl.value);
|
||||
return `${base}${publicChatPath.value}`;
|
||||
const query = new URLSearchParams();
|
||||
if (selectedPublishApiKey.value) {
|
||||
query.set('token', selectedPublishApiKey.value);
|
||||
}
|
||||
if (embed) {
|
||||
query.set('embed', '1');
|
||||
}
|
||||
const queryString = query.toString();
|
||||
if (!queryString) {
|
||||
return `${base}${publicChatPath.value}`;
|
||||
}
|
||||
return `${base}${publicChatPath.value}?${queryString}`;
|
||||
};
|
||||
const publicChatUrl = computed(() => {
|
||||
return buildPublicChatUrl(false);
|
||||
});
|
||||
const publicChatEmbedUrl = computed(() => {
|
||||
if (!publicChatUrl.value) {
|
||||
return '';
|
||||
}
|
||||
return `${publicChatUrl.value}?embed=1`;
|
||||
return buildPublicChatUrl(true);
|
||||
});
|
||||
const iframeCode = computed(() => {
|
||||
if (!publicChatEmbedUrl.value) {
|
||||
@@ -233,7 +262,9 @@ 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');
|
||||
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();
|
||||
}
|
||||
@@ -267,6 +298,84 @@ const getBotDetail = async () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
const formatApiKeyOptionLabel = (apiKey: string, expiredAt?: string) => {
|
||||
const normalized = String(apiKey || '').trim();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
const prefix = normalized.slice(0, 8);
|
||||
const suffix = normalized.slice(-6);
|
||||
const baseLabel =
|
||||
normalized.length > 18 ? `${prefix}...${suffix}` : normalized;
|
||||
return expiredAt ? `${baseLabel} · ${expiredAt}` : baseLabel;
|
||||
};
|
||||
const getPublishApiKeyOptions = async () => {
|
||||
const [resourceErr, resourceRes] = await tryit(api.get)(
|
||||
'/api/v1/sysApiKeyResourcePermission/list',
|
||||
);
|
||||
if (
|
||||
resourceErr ||
|
||||
resourceRes?.errorCode !== 0 ||
|
||||
!Array.isArray(resourceRes?.data)
|
||||
) {
|
||||
publishApiKeyOptions.value = [];
|
||||
return;
|
||||
}
|
||||
const publicChatResource = resourceRes.data.find(
|
||||
(item: any) => item?.requestInterface === '/public-api/bot/chat',
|
||||
);
|
||||
if (!publicChatResource?.id) {
|
||||
publishApiKeyOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const [apiKeyErr, apiKeyRes] = await tryit(api.get)(
|
||||
'/api/v1/sysApiKey/page',
|
||||
{
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 200,
|
||||
sortKey: 'created',
|
||||
sortType: 'desc',
|
||||
status: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (apiKeyErr || apiKeyRes?.errorCode !== 0) {
|
||||
publishApiKeyOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const records = apiKeyRes?.data?.records || [];
|
||||
const now = Date.now();
|
||||
publishApiKeyOptions.value = records
|
||||
.filter((item: any) => {
|
||||
if (item?.status !== 1) {
|
||||
return false;
|
||||
}
|
||||
if (item?.expiredAt && new Date(item.expiredAt).getTime() <= now) {
|
||||
return false;
|
||||
}
|
||||
const permissionIds = Array.isArray(item?.permissionIds)
|
||||
? item.permissionIds.map(String)
|
||||
: [];
|
||||
return permissionIds.includes(String(publicChatResource.id));
|
||||
})
|
||||
.map((item: any) => ({
|
||||
id: String(item.id || ''),
|
||||
apiKey: String(item.apiKey || ''),
|
||||
expiredAt: item.expiredAt,
|
||||
label: formatApiKeyOptionLabel(item.apiKey, item.expiredAt),
|
||||
}));
|
||||
if (
|
||||
selectedPublishApiKey.value &&
|
||||
!publishApiKeyOptions.value.some(
|
||||
(item) => item.apiKey === selectedPublishApiKey.value,
|
||||
)
|
||||
) {
|
||||
selectedPublishApiKey.value = '';
|
||||
}
|
||||
};
|
||||
const getLlmListData = async () => {
|
||||
const url = `/api/v1/model/list?modelType=chatModel&added=true`;
|
||||
api.get(url, {}).then((res) => {
|
||||
@@ -277,6 +386,7 @@ const getLlmListData = async () => {
|
||||
};
|
||||
onMounted(async () => {
|
||||
getPublishBaseUrl();
|
||||
getPublishApiKeyOptions();
|
||||
getAiBotPluginToolList();
|
||||
getAiBotKnowledgeList();
|
||||
getAiBotWorkflowList();
|
||||
@@ -285,9 +395,7 @@ onMounted(async () => {
|
||||
getLlmListData();
|
||||
});
|
||||
|
||||
const handleAnonymousAccessChange = (
|
||||
value: boolean | number | string,
|
||||
) => {
|
||||
const handleAnonymousAccessChange = (value: boolean | number | string) => {
|
||||
handleDialogOptionsStrChange('anonymousEnabled', value);
|
||||
};
|
||||
|
||||
@@ -681,14 +789,19 @@ const handleBasicInfoChange = async (
|
||||
key: 'alias' | 'categoryId' | 'title',
|
||||
value: any,
|
||||
) => {
|
||||
if (!botInfo.value || !props.hasSavePermission || syncingBasicInfoForm.value) {
|
||||
if (
|
||||
!botInfo.value ||
|
||||
!props.hasSavePermission ||
|
||||
syncingBasicInfoForm.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (updatingBasicInfo.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedValue = key === 'categoryId' ? value : String(value || '').trim();
|
||||
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;
|
||||
@@ -729,7 +842,7 @@ const handleBasicInfoChange = async (
|
||||
<div
|
||||
:class="[
|
||||
'bot-avatar-upload-wrap',
|
||||
(!hasSavePermission || updatingBotIcon) ? 'is-disabled' : '',
|
||||
!hasSavePermission || updatingBotIcon ? 'is-disabled' : '',
|
||||
]"
|
||||
>
|
||||
<UploadAvatar
|
||||
@@ -742,7 +855,9 @@ const handleBasicInfoChange = async (
|
||||
</div>
|
||||
<div class="bot-basic-form-panel">
|
||||
<div class="bot-basic-form-item">
|
||||
<span class="bot-basic-form-label">{{ $t('aiWorkflow.title') }}</span>
|
||||
<span class="bot-basic-form-label">{{
|
||||
$t('aiWorkflow.title')
|
||||
}}</span>
|
||||
<ElInput
|
||||
v-model="basicInfoForm.title"
|
||||
:disabled="!hasSavePermission || updatingBasicInfo"
|
||||
@@ -758,12 +873,16 @@ const handleBasicInfoChange = async (
|
||||
/>
|
||||
</div>
|
||||
<div class="bot-basic-form-item">
|
||||
<span class="bot-basic-form-label">{{ $t('aiWorkflow.categoryId') }}</span>
|
||||
<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)"
|
||||
@change="
|
||||
(value: any) => handleBasicInfoChange('categoryId', value)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1165,6 +1284,25 @@ const handleBasicInfoChange = async (
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="publish-external-item">
|
||||
<label class="publish-external-label">
|
||||
{{ $t('bot.chatAccessToken') }}
|
||||
</label>
|
||||
<ElSelect
|
||||
v-model="selectedPublishApiKey"
|
||||
clearable
|
||||
filterable
|
||||
class="w-full"
|
||||
:placeholder="$t('bot.chatAccessTokenPlaceholder')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in publishApiKeyOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.apiKey"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
<div class="publish-external-item">
|
||||
<label class="publish-external-label">
|
||||
{{ $t('bot.chatExternalLink') }}
|
||||
@@ -1182,11 +1320,7 @@ const handleBasicInfoChange = async (
|
||||
</ElIcon>
|
||||
{{ $t('bot.copyLink') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openPublicPage"
|
||||
>
|
||||
<ElButton size="small" type="primary" @click="openPublicPage">
|
||||
<ElIcon class="mr-1">
|
||||
<Link />
|
||||
</ElIcon>
|
||||
@@ -1197,15 +1331,16 @@ const handleBasicInfoChange = async (
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<ElIcon
|
||||
class="cursor-pointer text-gray-400 transition-colors hover:text-gray-600"
|
||||
>
|
||||
<InfoFilled />
|
||||
</ElIcon>
|
||||
</ElTooltip>
|
||||
</label>
|
||||
<div class="publish-external-code-preview">
|
||||
@@ -1447,6 +1582,12 @@ const handleBasicInfoChange = async (
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.publish-external-hint {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.publish-external-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -1490,7 +1631,8 @@ const handleBasicInfoChange = async (
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
|
||||
Reference in New Issue
Block a user