feat: 接入聊天历史界面与外链会话恢复

- 新增管理端与用户端聊天历史接口和页面

- 外链聊天支持访问令牌登录、身份保活与当前会话恢复

- 聊天执行链路切到统一 runtime 与 chatlog 查询接口
This commit is contained in:
2026-04-05 11:37:25 +08:00
parent 25e80433a5
commit a4f75a5e4c
48 changed files with 3724 additions and 972 deletions

View File

@@ -0,0 +1,279 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { ChatThinkingBlockProps } from './types';
defineOptions({
name: 'ChatThinkingBlock',
});
const props = withDefaults(defineProps<ChatThinkingBlockProps>(), {
content: '',
disabled: false,
emptyBehavior: 'hide',
expanded: false,
label: '',
readonly: false,
status: 'end',
summary: '',
});
const emit = defineEmits<{
'update:expanded': [boolean];
}>();
const normalizedContent = computed(() =>
String(props.content || '')
.replace(/\r\n/g, '\n')
.replace(/^\s*\n+/, '')
.trimEnd(),
);
const shouldRender = computed(
() =>
normalizedContent.value.length > 0 || props.emptyBehavior === 'placeholder',
);
const expandedModel = computed({
get: () => props.expanded,
set: (value: boolean) => emit('update:expanded', value),
});
const computedLabel = computed(() => {
if (props.label) {
return props.label;
}
if (props.status === 'thinking') {
return '思考中';
}
if (props.status === 'error') {
return '思考异常';
}
return '已思考';
});
const computedSummary = computed(() => {
if (props.summary) {
return props.summary;
}
const source = normalizedContent.value
.split('\n')
.map((line) => line.trim())
.find(Boolean);
if (!source) {
return props.emptyBehavior === 'placeholder' ? '暂无思考内容' : '';
}
return source.length > 56 ? `${source.slice(0, 56)}...` : source;
});
const canToggle = computed(
() => !props.disabled && normalizedContent.value.length > 0,
);
function toggleExpanded() {
if (!canToggle.value) {
return;
}
expandedModel.value = !expandedModel.value;
}
</script>
<template>
<div
v-if="shouldRender"
class="chat-thinking-block"
:class="[
`is-${status}`,
{
'is-disabled': disabled,
'is-expanded': expandedModel,
'is-readonly': readonly,
},
]"
>
<button
type="button"
class="chat-thinking-block__trigger"
:disabled="!canToggle"
@click="toggleExpanded"
>
<span class="chat-thinking-block__leading">
<span class="chat-thinking-block__indicator" aria-hidden="true"></span>
<span class="chat-thinking-block__label">{{ computedLabel }}</span>
</span>
<span
v-if="!expandedModel && computedSummary"
class="chat-thinking-block__summary"
>
{{ computedSummary }}
</span>
<span
class="chat-thinking-block__chevron"
:class="{ 'is-open': expandedModel }"
aria-hidden="true"
></span>
</button>
<transition name="chat-thinking-block__body-transition">
<div
v-if="expandedModel && normalizedContent"
class="chat-thinking-block__body"
>
<div class="chat-thinking-block__content">
{{ normalizedContent }}
</div>
</div>
</transition>
</div>
</template>
<style scoped>
.chat-thinking-block {
border: 1px solid hsl(var(--divider-faint) / 0.18);
border-radius: 16px;
background:
linear-gradient(
180deg,
hsl(var(--glass-tint) / 0.48) 0%,
hsl(var(--surface-panel) / 0.74) 100%
);
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.24),
0 10px 24px -24px hsl(var(--foreground) / 0.18);
backdrop-filter: blur(12px);
}
.chat-thinking-block__trigger {
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 9px 12px;
color: inherit;
text-align: left;
background: transparent;
border: 0;
border-radius: inherit;
transition: background-color 0.18s ease;
}
.chat-thinking-block__trigger:not(:disabled) {
cursor: pointer;
}
.chat-thinking-block__trigger:not(:disabled):hover {
background: hsl(var(--surface-contrast-soft) / 0.34);
}
.chat-thinking-block__trigger:disabled {
cursor: default;
}
.chat-thinking-block__leading {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 8px;
}
.chat-thinking-block__indicator {
position: relative;
flex: 0 0 auto;
width: 8px;
height: 8px;
border-radius: 999px;
background: hsl(var(--text-muted) / 0.74);
}
.chat-thinking-block.is-thinking .chat-thinking-block__indicator {
background: hsl(var(--primary) / 0.82);
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
animation: chat-thinking-pulse 1.8s ease-in-out infinite;
}
.chat-thinking-block.is-error .chat-thinking-block__indicator {
background: hsl(var(--destructive) / 0.86);
box-shadow: 0 0 0 4px hsl(var(--destructive) / 0.1);
}
.chat-thinking-block__label {
font-size: 12px;
font-weight: 600;
line-height: 1.2;
color: hsl(var(--text-strong));
white-space: nowrap;
}
.chat-thinking-block__summary {
min-width: 0;
overflow: hidden;
font-size: 12px;
line-height: 1.3;
color: hsl(var(--text-muted));
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-thinking-block__chevron {
width: 9px;
height: 9px;
border-right: 1.5px solid hsl(var(--text-muted));
border-bottom: 1.5px solid hsl(var(--text-muted));
transform: rotate(45deg) translateY(-1px);
transition: transform 0.18s ease;
}
.chat-thinking-block__chevron.is-open {
transform: rotate(225deg) translateY(-1px);
}
.chat-thinking-block__body {
padding: 0 12px 12px;
}
.chat-thinking-block__content {
margin: 0;
padding: 10px 12px;
border-radius: 12px;
background: hsl(var(--surface-panel) / 0.72);
font-size: 12px;
line-height: 1.68;
color: hsl(var(--text-secondary));
white-space: pre-wrap;
word-break: break-word;
}
.chat-thinking-block.is-disabled {
opacity: 0.82;
}
.chat-thinking-block__body-transition-enter-active,
.chat-thinking-block__body-transition-leave-active {
transition:
opacity 0.18s ease,
transform 0.18s ease;
}
.chat-thinking-block__body-transition-enter-from,
.chat-thinking-block__body-transition-leave-to {
opacity: 0;
transform: translateY(-4px);
}
@keyframes chat-thinking-pulse {
0%,
100% {
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
opacity: 0.92;
}
50% {
box-shadow: 0 0 0 7px hsl(var(--primary) / 0.04);
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,5 @@
export { default as ChatThinkingBlock } from './ChatThinkingBlock.vue';
export type {
ChatThinkingBlockProps,
ChatThinkingBlockStatus,
} from './types';

View File

@@ -0,0 +1,12 @@
export type ChatThinkingBlockStatus = 'end' | 'error' | 'thinking';
export interface ChatThinkingBlockProps {
content?: string;
disabled?: boolean;
emptyBehavior?: 'hide' | 'placeholder';
expanded?: boolean;
label?: string;
readonly?: boolean;
status?: ChatThinkingBlockStatus;
summary?: string;
}

View File

@@ -1,5 +1,6 @@
export * from './api-component';
export * from './captcha';
export * from './chat-thinking';
export * from './col-page';
export * from './count-to';
export * from './ellipsis-text';

View File

@@ -1,17 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="16px" viewBox="0 0 18 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>聊天助理备份 7</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="聊天记录" transform="translate(-29, -463)">
<g id="编组-12备份-2" transform="translate(0, 3)">
<g id="编组-32备份-2" transform="translate(12, 444)">
<g id="聊天助理备份-7" transform="translate(16, 14)">
<rect id="矩形" x="0" y="0" width="20" height="20"></rect>
<path d="M15,2.8 C15.8836556,2.8 16.6836556,3.1581722 17.2627417,3.7372583 C17.8418278,4.3163444 18.2,5.1163444 18.2,6 L18.2,17.2 L5,17.2 C4.1163444,17.2 3.3163444,16.8418278 2.7372583,16.2627417 C2.1581722,15.6836556 1.8,14.8836556 1.8,14 L1.8,6 C1.8,5.1163444 2.1581722,4.3163444 2.7372583,3.7372583 C3.3163444,3.1581722 4.1163444,2.8 5,2.8 Z" id="形状结合" stroke="currentColor" stroke-width="1.6"></path>
<polyline id="路径-2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" points="8.8275 6.93789684 8.8275 10.9003453 12.5 10.9003453"></polyline>
</g>
</g>
</g>
</g>
</g>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.25"
>
<path d="M3 3.4h7.9A1.9 1.9 0 0 1 12.8 5.3v3.5a1.9 1.9 0 0 1-1.9 1.9H7.2l-2.4 2V10.7H4.9A1.9 1.9 0 0 1 3 8.8V3.4Z"/>
<path d="M5.3 5.9h2.2"/>
<circle cx="9.6" cy="7.2" r="1.95"/>
<path d="M9.6 6.2v1.2l.85.58"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -10,6 +10,7 @@ const SvgDownloadIcon = createIconifyIcon('svg:download');
const SvgCardIcon = createIconifyIcon('svg:card');
const SvgBellIcon = createIconifyIcon('svg:bell');
const SvgCakeIcon = createIconifyIcon('svg:cake');
const SvgChatHistoryIcon = createIconifyIcon('svg:chat-history');
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
const SvgGithubIcon = createIconifyIcon('svg:github');
const SvgGoogleIcon = createIconifyIcon('svg:google');
@@ -27,6 +28,7 @@ export {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgChatHistoryIcon,
SvgDingDingIcon,
SvgDownloadIcon,
SvgGithubIcon,