Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5b3ce8424 | |||
| 80cbbfa5ca | |||
| 9177bb660f | |||
| a3df39a01a | |||
| 25dce05cbb | |||
| 1542ea3e03 |
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.1.5"
|
||||
VERSION = "4.1.6"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
|
||||
@@ -285,11 +285,11 @@ async def run_agent(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
astr_event.set_result(
|
||||
MessageEventResult().message(
|
||||
f"AstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {str(e)}\n\n请在控制台查看和分享错误详情。\n"
|
||||
)
|
||||
)
|
||||
err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {str(e)}\n\n请在控制台查看和分享错误详情。\n"
|
||||
if agent_runner.streaming:
|
||||
yield MessageChain().message(err_msg)
|
||||
else:
|
||||
astr_event.set_result(MessageEventResult().message(err_msg))
|
||||
return
|
||||
asyncio.create_task(
|
||||
Metric.upload(
|
||||
|
||||
@@ -7,7 +7,13 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
|
||||
from .entities import ProviderType
|
||||
from .provider import Provider, STTProvider, TTSProvider, EmbeddingProvider
|
||||
from .provider import (
|
||||
Provider,
|
||||
STTProvider,
|
||||
TTSProvider,
|
||||
EmbeddingProvider,
|
||||
RerankProvider,
|
||||
)
|
||||
from .register import llm_tools, provider_cls_map
|
||||
from ..persona_mgr import PersonaManager
|
||||
|
||||
@@ -38,7 +44,12 @@ class ProviderManager:
|
||||
"""加载的 Text To Speech Provider 的实例"""
|
||||
self.embedding_provider_insts: List[EmbeddingProvider] = []
|
||||
"""加载的 Embedding Provider 的实例"""
|
||||
self.inst_map: dict[str, Provider | STTProvider | TTSProvider] = {}
|
||||
self.rerank_provider_insts: List[RerankProvider] = []
|
||||
"""加载的 Rerank Provider 的实例"""
|
||||
self.inst_map: dict[
|
||||
str,
|
||||
Provider | STTProvider | TTSProvider | EmbeddingProvider | RerankProvider,
|
||||
] = {}
|
||||
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
|
||||
self.llm_tools = llm_tools
|
||||
|
||||
@@ -378,14 +389,16 @@ class ProviderManager:
|
||||
if not self.curr_provider_inst:
|
||||
self.curr_provider_inst = inst
|
||||
|
||||
elif provider_metadata.provider_type in [
|
||||
ProviderType.EMBEDDING,
|
||||
ProviderType.RERANK,
|
||||
]:
|
||||
elif provider_metadata.provider_type == ProviderType.EMBEDDING:
|
||||
inst = cls_type(provider_config, self.provider_settings)
|
||||
if getattr(inst, "initialize", None):
|
||||
await inst.initialize()
|
||||
self.embedding_provider_insts.append(inst)
|
||||
elif provider_metadata.provider_type == ProviderType.RERANK:
|
||||
inst = cls_type(provider_config, self.provider_settings)
|
||||
if getattr(inst, "initialize", None):
|
||||
await inst.initialize()
|
||||
self.rerank_provider_insts.append(inst)
|
||||
|
||||
self.inst_map[provider_config["id"]] = inst
|
||||
except Exception as e:
|
||||
|
||||
@@ -6,6 +6,7 @@ from astrbot.core.provider.provider import (
|
||||
TTSProvider,
|
||||
STTProvider,
|
||||
EmbeddingProvider,
|
||||
RerankProvider,
|
||||
)
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.db import BaseDatabase
|
||||
@@ -103,11 +104,13 @@ class Context:
|
||||
"""
|
||||
self.provider_manager.provider_insts.append(provider)
|
||||
|
||||
def get_provider_by_id(self, provider_id: str) -> Provider | None:
|
||||
"""通过 ID 获取对应的 LLM Provider(Chat_Completion 类型)。"""
|
||||
def get_provider_by_id(
|
||||
self, provider_id: str
|
||||
) -> (
|
||||
Provider | TTSProvider | STTProvider | EmbeddingProvider | RerankProvider | None
|
||||
):
|
||||
"""通过 ID 获取对应的 LLM Provider。"""
|
||||
prov = self.provider_manager.inst_map.get(provider_id)
|
||||
if prov and not isinstance(prov, Provider):
|
||||
raise ValueError("返回的 Provider 不是 Provider 类型")
|
||||
return prov
|
||||
|
||||
def get_all_providers(self) -> List[Provider]:
|
||||
|
||||
@@ -227,9 +227,11 @@ async def download_dashboard(
|
||||
path = os.path.join(get_astrbot_data_path(), "dashboard.zip")
|
||||
|
||||
if latest or len(str(version)) != 40:
|
||||
logger.info(f"准备下载 {version} 发行版本的 AstrBot WebUI 文件")
|
||||
ver_name = "latest" if latest else version
|
||||
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
|
||||
logger.info(
|
||||
f"准备下载指定发行版本的 AstrBot WebUI 文件: {dashboard_release_url}"
|
||||
)
|
||||
try:
|
||||
await download_file(dashboard_release_url, path, show_progress=True)
|
||||
except BaseException as _:
|
||||
@@ -241,24 +243,10 @@ async def download_dashboard(
|
||||
dashboard_release_url = f"{proxy}/{dashboard_release_url}"
|
||||
await download_file(dashboard_release_url, path, show_progress=True)
|
||||
else:
|
||||
logger.info(f"准备下载指定版本的 AstrBot WebUI: {version}")
|
||||
|
||||
url = (
|
||||
"https://api.github.com/repos/AstrBotDevs/astrbot-release-harbour/releases"
|
||||
)
|
||||
url = f"https://github.com/AstrBotDevs/astrbot-release-harbour/releases/download/release-{version}/dist.zip"
|
||||
logger.info(f"准备下载指定版本的 AstrBot WebUI: {url}")
|
||||
if proxy:
|
||||
url = f"{proxy}/{url}"
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
releases = await resp.json()
|
||||
for release in releases:
|
||||
if version in release["tag_name"]:
|
||||
download_url = release["assets"][0]["browser_download_url"]
|
||||
await download_file(download_url, path, show_progress=True)
|
||||
else:
|
||||
logger.warning(f"未找到指定的版本的 Dashboard 构建文件: {version}")
|
||||
return
|
||||
|
||||
await download_file(url, path, show_progress=True)
|
||||
with zipfile.ZipFile(path, "r") as z:
|
||||
z.extractall(extract_path)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# What's Changed
|
||||
|
||||
1. fix: 修复在某些情况下,出现 「返回的 Provider 不是 Provider 类型的错误」
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,775 @@
|
||||
<template>
|
||||
<div class="messages-container" ref="messageContainer">
|
||||
<!-- 聊天消息列表 -->
|
||||
<div class="message-list">
|
||||
<div class="message-item fade-in" v-for="(msg, index) in messages" :key="index">
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.content.type == 'user'" class="user-message">
|
||||
<div class="message-bubble user-bubble" :class="{ 'has-audio': msg.content.audio_url }"
|
||||
:style="{ backgroundColor: isDark ? '#2d2e30' : '#e7ebf4' }">
|
||||
<pre
|
||||
style="font-family: inherit; white-space: pre-wrap; word-wrap: break-word;">{{ msg.content.message }}</pre>
|
||||
|
||||
<!-- 图片附件 -->
|
||||
<div class="image-attachments" v-if="msg.content.image_url && msg.content.image_url.length > 0">
|
||||
<div v-for="(img, index) in msg.content.image_url" :key="index" class="image-attachment">
|
||||
<img :src="img" class="attached-image" @click="$emit('openImagePreview', img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音频附件 -->
|
||||
<div class="audio-attachment" v-if="msg.content.audio_url && msg.content.audio_url.length > 0">
|
||||
<audio controls class="audio-player">
|
||||
<source :src="msg.content.audio_url" type="audio/wav">
|
||||
{{ t('messages.errors.browser.audioNotSupported') }}
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bot Messages -->
|
||||
<div v-else class="bot-message">
|
||||
<div v-if="isStreaming && index === messages.length - 1" style="width: 36px; height: 36px;">
|
||||
<v-progress-circular indeterminate size="28" width="2"
|
||||
style="margin-top: 16px;"></v-progress-circular>
|
||||
</div>
|
||||
<v-avatar v-else class="bot-avatar" size="36">
|
||||
<span class="text-h2">✨</span>
|
||||
</v-avatar>
|
||||
<div class="bot-message-content">
|
||||
<div class="message-bubble bot-bubble">
|
||||
<!-- Text -->
|
||||
<div v-if="msg.content.message && msg.content.message.trim()"
|
||||
v-html="md.render(msg.content.message)" class="markdown-content"></div>
|
||||
|
||||
<!-- Image -->
|
||||
<div class="embedded-images"
|
||||
v-if="msg.content.embedded_images && msg.content.embedded_images.length > 0">
|
||||
<div v-for="(img, imgIndex) in msg.content.embedded_images" :key="imgIndex"
|
||||
class="embedded-image">
|
||||
<img :src="img" class="bot-embedded-image"
|
||||
@click="$emit('openImagePreview', img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio -->
|
||||
<div class="embedded-audio" v-if="msg.content.embedded_audio">
|
||||
<audio controls class="audio-player">
|
||||
<source :src="msg.content.embedded_audio" type="audio/wav">
|
||||
{{ t('messages.errors.browser.audioNotSupported') }}
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-actions">
|
||||
<v-btn :icon="getCopyIcon(index)" size="small" variant="text" class="copy-message-btn"
|
||||
:class="{ 'copy-success': isCopySuccess(index) }"
|
||||
@click="copyBotMessage(msg.content.message, index)" :title="t('core.common.copy')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: false,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (err) {
|
||||
console.error('Highlight error:', err);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'MessageList',
|
||||
props: {
|
||||
messages: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
isDark: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['openImagePreview'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/chat');
|
||||
|
||||
return {
|
||||
t,
|
||||
tm,
|
||||
md
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copiedMessages: new Set(),
|
||||
isUserNearBottom: true,
|
||||
scrollThreshold: 1,
|
||||
scrollTimer: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
this.addScrollListener();
|
||||
this.scrollToBottom();
|
||||
},
|
||||
updated() {
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
if (this.isUserNearBottom) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 复制代码到剪贴板
|
||||
copyCodeToClipboard(code) {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
console.log('代码已复制到剪贴板');
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 如果现代API失败,使用传统方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = code;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
console.log('代码已复制到剪贴板 (fallback)');
|
||||
} catch (fallbackErr) {
|
||||
console.error('复制失败 (fallback):', fallbackErr);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
},
|
||||
|
||||
// 复制bot消息到剪贴板
|
||||
copyBotMessage(message, messageIndex) {
|
||||
// 获取对应的消息对象
|
||||
const msgObj = this.messages[messageIndex].content;
|
||||
let textToCopy = '';
|
||||
|
||||
// 如果有文本消息,添加到复制内容中
|
||||
if (message && message.trim()) {
|
||||
// 移除HTML标签,获取纯文本
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = message;
|
||||
textToCopy = tempDiv.textContent || tempDiv.innerText || message;
|
||||
}
|
||||
|
||||
// 如果有内嵌图片,添加说明
|
||||
if (msgObj && msgObj.embedded_images && msgObj.embedded_images.length > 0) {
|
||||
if (textToCopy) textToCopy += '\n\n';
|
||||
textToCopy += `[包含 ${msgObj.embedded_images.length} 张图片]`;
|
||||
}
|
||||
|
||||
// 如果有内嵌音频,添加说明
|
||||
if (msgObj && msgObj.embedded_audio) {
|
||||
if (textToCopy) textToCopy += '\n\n';
|
||||
textToCopy += '[包含音频内容]';
|
||||
}
|
||||
|
||||
// 如果没有任何内容,使用默认文本
|
||||
if (!textToCopy.trim()) {
|
||||
textToCopy = '[媒体内容]';
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
console.log('消息已复制到剪贴板');
|
||||
this.showCopySuccess(messageIndex);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 如果现代API失败,使用传统方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = textToCopy;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
console.log('消息已复制到剪贴板 (fallback)');
|
||||
this.showCopySuccess(messageIndex);
|
||||
} catch (fallbackErr) {
|
||||
console.error('复制失败 (fallback):', fallbackErr);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
},
|
||||
|
||||
// 显示复制成功提示
|
||||
showCopySuccess(messageIndex) {
|
||||
this.copiedMessages.add(messageIndex);
|
||||
|
||||
// 2秒后移除成功状态
|
||||
setTimeout(() => {
|
||||
this.copiedMessages.delete(messageIndex);
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
// 获取复制按钮图标
|
||||
getCopyIcon(messageIndex) {
|
||||
return this.copiedMessages.has(messageIndex) ? 'mdi-check' : 'mdi-content-copy';
|
||||
},
|
||||
|
||||
// 检查是否为复制成功状态
|
||||
isCopySuccess(messageIndex) {
|
||||
return this.copiedMessages.has(messageIndex);
|
||||
},
|
||||
|
||||
// 获取复制图标SVG
|
||||
getCopyIconSvg() {
|
||||
return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
||||
},
|
||||
|
||||
// 获取成功图标SVG
|
||||
getSuccessIconSvg() {
|
||||
return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20,6 9,17 4,12"></polyline></svg>';
|
||||
},
|
||||
|
||||
// 初始化代码块复制按钮
|
||||
initCodeCopyButtons() {
|
||||
this.$nextTick(() => {
|
||||
const codeBlocks = this.$refs.messageContainer?.querySelectorAll('pre code') || [];
|
||||
codeBlocks.forEach((codeBlock, index) => {
|
||||
const pre = codeBlock.parentElement;
|
||||
if (pre && !pre.querySelector('.copy-code-btn')) {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'copy-code-btn';
|
||||
button.innerHTML = this.getCopyIconSvg();
|
||||
button.title = '复制代码';
|
||||
button.addEventListener('click', () => {
|
||||
this.copyCodeToClipboard(codeBlock.textContent);
|
||||
// 显示复制成功提示
|
||||
button.innerHTML = this.getSuccessIconSvg();
|
||||
button.style.color = '#4caf50';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = this.getCopyIconSvg();
|
||||
button.style.color = '';
|
||||
}, 2000);
|
||||
});
|
||||
pre.style.position = 'relative';
|
||||
pre.appendChild(button);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
initImageClickEvents() {
|
||||
this.$nextTick(() => {
|
||||
// 查找所有动态生成的图片(在markdown-content中)
|
||||
const images = document.querySelectorAll('.markdown-content img');
|
||||
images.forEach((img) => {
|
||||
if (!img.hasAttribute('data-click-enabled')) {
|
||||
img.style.cursor = 'pointer';
|
||||
img.setAttribute('data-click-enabled', 'true');
|
||||
img.onclick = () => this.$emit('openImagePreview', img.src);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.messageContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
this.isUserNearBottom = true; // 程序滚动到底部后标记用户在底部
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 添加滚动事件监听器
|
||||
addScrollListener() {
|
||||
const container = this.$refs.messageContainer;
|
||||
if (container) {
|
||||
container.addEventListener('scroll', this.throttledHandleScroll);
|
||||
}
|
||||
},
|
||||
|
||||
// 节流处理滚动事件
|
||||
throttledHandleScroll() {
|
||||
if (this.scrollTimer) return;
|
||||
|
||||
this.scrollTimer = setTimeout(() => {
|
||||
this.handleScroll();
|
||||
this.scrollTimer = null;
|
||||
}, 50); // 50ms 节流
|
||||
},
|
||||
|
||||
// 处理滚动事件
|
||||
handleScroll() {
|
||||
const container = this.$refs.messageContainer;
|
||||
if (container) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
|
||||
|
||||
// 判断用户是否在底部附近
|
||||
this.isUserNearBottom = distanceFromBottom <= this.scrollThreshold;
|
||||
}
|
||||
},
|
||||
|
||||
// 组件销毁时移除监听器
|
||||
beforeUnmount() {
|
||||
const container = this.$refs.messageContainer;
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', this.throttledHandleScroll);
|
||||
}
|
||||
// 清理定时器
|
||||
if (this.scrollTimer) {
|
||||
clearTimeout(this.scrollTimer);
|
||||
this.scrollTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 消息列表样式 */
|
||||
.message-list {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 24px;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bot-message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
max-width: 80%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.bot-message:hover .message-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-message-btn {
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--v-theme-secondary);
|
||||
}
|
||||
|
||||
.copy-message-btn:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(103, 58, 183, 0.1);
|
||||
}
|
||||
|
||||
.copy-message-btn.copy-success {
|
||||
color: #4caf50;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-message-btn.copy-success:hover {
|
||||
color: #4caf50;
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
color: var(--v-theme-primaryText);
|
||||
padding: 18px 20px;
|
||||
font-size: 15px;
|
||||
max-width: 60%;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.bot-bubble {
|
||||
border: 1px solid var(--v-theme-border);
|
||||
color: var(--v-theme-primaryText);
|
||||
font-size: 15px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.user-avatar,
|
||||
.bot-avatar {
|
||||
align-self: flex-start;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 附件样式 */
|
||||
.image-attachments {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-attachment {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.attached-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.audio-attachment {
|
||||
margin-top: 8px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* 包含音频的消息气泡最小宽度 */
|
||||
.message-bubble.has-audio {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.embedded-images {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.embedded-image {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bot-embedded-image {
|
||||
max-width: 80%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.bot-embedded-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.embedded-audio {
|
||||
width: 300px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.embedded-audio .audio-player {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* 动画类 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Markdown内容样式 - 需要全局样式 */
|
||||
.markdown-content {
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--v-theme-primaryText);
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 1.8em;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-left: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: var(--v-theme-surface);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: rgb(var(--v-theme-codeBg));
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--v-theme-code);
|
||||
}
|
||||
|
||||
/* 代码块中的code标签样式 */
|
||||
.markdown-content pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
color: inherit;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 自定义代码高亮样式 */
|
||||
.markdown-content pre {
|
||||
border: 1px solid var(--v-theme-border);
|
||||
background-color: rgb(var(--v-theme-preBg));
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 确保highlight.js的样式正确应用 */
|
||||
.markdown-content pre code.hljs {
|
||||
background: transparent !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* 亮色主题下的代码高亮 */
|
||||
.v-theme--light .markdown-content pre {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
/* 暗色主题下的代码块样式 */
|
||||
.v-theme--dark .markdown-content pre {
|
||||
background-color: #0d1117 !important;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.v-theme--dark .markdown-content pre code {
|
||||
color: #e6edf3 !important;
|
||||
}
|
||||
|
||||
/* 暗色主题下的highlight.js样式覆盖 */
|
||||
.v-theme--dark .hljs {
|
||||
background: #0d1117 !important;
|
||||
color: #e6edf3 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-keyword,
|
||||
.v-theme--dark .hljs-selector-tag,
|
||||
.v-theme--dark .hljs-built_in,
|
||||
.v-theme--dark .hljs-name,
|
||||
.v-theme--dark .hljs-tag {
|
||||
color: #ff7b72 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-string,
|
||||
.v-theme--dark .hljs-title,
|
||||
.v-theme--dark .hljs-section,
|
||||
.v-theme--dark .hljs-attribute,
|
||||
.v-theme--dark .hljs-literal,
|
||||
.v-theme--dark .hljs-template-tag,
|
||||
.v-theme--dark .hljs-template-variable,
|
||||
.v-theme--dark .hljs-type,
|
||||
.v-theme--dark .hljs-addition {
|
||||
color: #a5d6ff !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-comment,
|
||||
.v-theme--dark .hljs-quote,
|
||||
.v-theme--dark .hljs-deletion,
|
||||
.v-theme--dark .hljs-meta {
|
||||
color: #8b949e !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-number,
|
||||
.v-theme--dark .hljs-regexp,
|
||||
.v-theme--dark .hljs-symbol,
|
||||
.v-theme--dark .hljs-variable,
|
||||
.v-theme--dark .hljs-template-variable,
|
||||
.v-theme--dark .hljs-link,
|
||||
.v-theme--dark .hljs-selector-attr,
|
||||
.v-theme--dark .hljs-selector-pseudo {
|
||||
color: #79c0ff !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .hljs-function,
|
||||
.v-theme--dark .hljs-class,
|
||||
.v-theme--dark .hljs-title.class_ {
|
||||
color: #d2a8ff !important;
|
||||
}
|
||||
|
||||
/* 复制按钮样式 */
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.copy-code-btn:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
color: #333;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.copy-code-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.markdown-content pre:hover .copy-code-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.v-theme--dark .copy-code-btn {
|
||||
background: rgba(45, 45, 45, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.v-theme--dark .copy-code-btn:hover {
|
||||
background: rgba(45, 45, 45, 1);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid var(--v-theme-secondary);
|
||||
padding-left: 16px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content th,
|
||||
.markdown-content td {
|
||||
border: 1px solid var(--v-theme-background);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background-color: var(--v-theme-containerBg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
title: String
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card variant="outlined" elevation="0" class="withbg">
|
||||
<v-card-item>
|
||||
<div class="d-sm-flex align-center justify-space-between">
|
||||
<v-card-title>{{ props.title }}</v-card-title>
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
</v-card-item>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<slot />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -1,22 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed} from 'vue';
|
||||
import {useCustomizerStore} from '@/stores/customizer';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import axios from 'axios';
|
||||
import Logo from '@/components/shared/Logo.vue';
|
||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
import {md5} from 'js-md5';
|
||||
import {useAuthStore} from '@/stores/auth';
|
||||
import {useCommonStore} from '@/stores/common';
|
||||
import { md5 } from 'js-md5';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import { router } from '@/router';
|
||||
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号
|
||||
});
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
@@ -44,11 +44,11 @@ let installLoading = ref(false);
|
||||
let tab = ref(0);
|
||||
|
||||
const releasesHeader = computed(() => [
|
||||
{title: t('core.header.updateDialog.table.tag'), key: 'tag_name'},
|
||||
{title: t('core.header.updateDialog.table.publishDate'), key: 'published_at'},
|
||||
{title: t('core.header.updateDialog.table.content'), key: 'body'},
|
||||
{title: t('core.header.updateDialog.table.sourceUrl'), key: 'zipball_url'},
|
||||
{title: t('core.header.updateDialog.table.actions'), key: 'switch'}
|
||||
{ title: t('core.header.updateDialog.table.tag'), key: 'tag_name' },
|
||||
{ title: t('core.header.updateDialog.table.publishDate'), key: 'published_at' },
|
||||
{ title: t('core.header.updateDialog.table.content'), key: 'body' },
|
||||
{ title: t('core.header.updateDialog.table.sourceUrl'), key: 'zipball_url' },
|
||||
{ title: t('core.header.updateDialog.table.actions'), key: 'switch' }
|
||||
]);
|
||||
|
||||
// Form validation
|
||||
@@ -103,90 +103,90 @@ function accountEdit() {
|
||||
new_password: newPassword.value,
|
||||
new_username: newUsername.value ? newUsername.value : username
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.status == 'error') {
|
||||
accountEditStatus.value.error = true;
|
||||
accountEditStatus.value.message = res.data.message;
|
||||
password.value = '';
|
||||
newPassword.value = '';
|
||||
return;
|
||||
}
|
||||
accountEditStatus.value.success = true;
|
||||
accountEditStatus.value.message = res.data.message;
|
||||
setTimeout(() => {
|
||||
dialog.value = !dialog.value;
|
||||
const authStore = useAuthStore();
|
||||
authStore.logout();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
.then((res) => {
|
||||
if (res.data.status == 'error') {
|
||||
accountEditStatus.value.error = true;
|
||||
accountEditStatus.value.message = typeof err === 'string' ? err : t('core.header.accountDialog.messages.updateFailed');
|
||||
accountEditStatus.value.message = res.data.message;
|
||||
password.value = '';
|
||||
newPassword.value = '';
|
||||
})
|
||||
.finally(() => {
|
||||
accountEditStatus.value.loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
accountEditStatus.value.success = true;
|
||||
accountEditStatus.value.message = res.data.message;
|
||||
setTimeout(() => {
|
||||
dialog.value = !dialog.value;
|
||||
const authStore = useAuthStore();
|
||||
authStore.logout();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
accountEditStatus.value.error = true;
|
||||
accountEditStatus.value.message = typeof err === 'string' ? err : t('core.header.accountDialog.messages.updateFailed');
|
||||
password.value = '';
|
||||
newPassword.value = '';
|
||||
})
|
||||
.finally(() => {
|
||||
accountEditStatus.value.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function getVersion() {
|
||||
axios.get('/api/stat/version')
|
||||
.then((res) => {
|
||||
botCurrVersion.value = "v" + res.data.data.version;
|
||||
dashboardCurrentVersion.value = res.data.data?.dashboard_version;
|
||||
let change_pwd_hint = res.data.data?.change_pwd_hint;
|
||||
if (change_pwd_hint) {
|
||||
dialog.value = true;
|
||||
accountWarning.value = true;
|
||||
localStorage.setItem('change_pwd_hint', 'true');
|
||||
} else {
|
||||
localStorage.removeItem('change_pwd_hint');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
.then((res) => {
|
||||
botCurrVersion.value = "v" + res.data.data.version;
|
||||
dashboardCurrentVersion.value = res.data.data?.dashboard_version;
|
||||
let change_pwd_hint = res.data.data?.change_pwd_hint;
|
||||
if (change_pwd_hint) {
|
||||
dialog.value = true;
|
||||
accountWarning.value = true;
|
||||
localStorage.setItem('change_pwd_hint', 'true');
|
||||
} else {
|
||||
localStorage.removeItem('change_pwd_hint');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
function checkUpdate() {
|
||||
updateStatus.value = t('core.header.updateDialog.status.checking');
|
||||
axios.get('/api/update/check')
|
||||
.then((res) => {
|
||||
hasNewVersion.value = res.data.data.has_new_version;
|
||||
.then((res) => {
|
||||
hasNewVersion.value = res.data.data.has_new_version;
|
||||
|
||||
if (res.data.data.has_new_version) {
|
||||
releaseMessage.value = res.data.message;
|
||||
updateStatus.value = t('core.header.version.hasNewVersion');
|
||||
} else {
|
||||
updateStatus.value = res.data.message;
|
||||
}
|
||||
dashboardHasNewVersion.value = res.data.data.dashboard_has_new_version;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response && err.response.status == 401) {
|
||||
console.log("401");
|
||||
const authStore = useAuthStore();
|
||||
authStore.logout();
|
||||
return;
|
||||
}
|
||||
console.log(err);
|
||||
updateStatus.value = err
|
||||
});
|
||||
if (res.data.data.has_new_version) {
|
||||
releaseMessage.value = res.data.message;
|
||||
updateStatus.value = t('core.header.version.hasNewVersion');
|
||||
} else {
|
||||
updateStatus.value = res.data.message;
|
||||
}
|
||||
dashboardHasNewVersion.value = res.data.data.dashboard_has_new_version;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response && err.response.status == 401) {
|
||||
console.log("401");
|
||||
const authStore = useAuthStore();
|
||||
authStore.logout();
|
||||
return;
|
||||
}
|
||||
console.log(err);
|
||||
updateStatus.value = err
|
||||
});
|
||||
}
|
||||
|
||||
function getReleases() {
|
||||
axios.get('/api/update/releases')
|
||||
.then((res) => {
|
||||
releases.value = res.data.data.map((item: any) => {
|
||||
item.published_at = new Date(item.published_at).toLocaleString();
|
||||
return item;
|
||||
})
|
||||
.then((res) => {
|
||||
releases.value = res.data.data.map((item: any) => {
|
||||
item.published_at = new Date(item.published_at).toLocaleString();
|
||||
return item;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
function getDevCommits() {
|
||||
@@ -209,10 +209,10 @@ function getDevCommits() {
|
||||
.then(data => {
|
||||
devCommits.value = Array.isArray(data)
|
||||
? data.map((commit: any) => ({
|
||||
sha: commit.sha,
|
||||
date: new Date(commit.commit.author.date).toLocaleString(),
|
||||
message: commit.commit.message
|
||||
}))
|
||||
sha: commit.sha,
|
||||
date: new Date(commit.commit.author.date).toLocaleString(),
|
||||
message: commit.commit.message
|
||||
}))
|
||||
: [];
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -239,40 +239,40 @@ function switchVersion(version: string) {
|
||||
version: version,
|
||||
proxy: localStorage.getItem('selectedGitHubProxy') || ''
|
||||
})
|
||||
.then((res) => {
|
||||
updateStatus.value = res.data.message;
|
||||
if (res.data.status == 'ok') {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
updateStatus.value = err
|
||||
}).finally(() => {
|
||||
installLoading.value = false;
|
||||
});
|
||||
.then((res) => {
|
||||
updateStatus.value = res.data.message;
|
||||
if (res.data.status == 'ok') {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
updateStatus.value = err
|
||||
}).finally(() => {
|
||||
installLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateDashboard() {
|
||||
updatingDashboardLoading.value = true;
|
||||
updateStatus.value = t('core.header.updateDialog.status.updating');
|
||||
axios.post('/api/update/dashboard')
|
||||
.then((res) => {
|
||||
updateStatus.value = res.data.message;
|
||||
if (res.data.status == 'ok') {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
updateStatus.value = err
|
||||
}).finally(() => {
|
||||
updatingDashboardLoading.value = false;
|
||||
});
|
||||
.then((res) => {
|
||||
updateStatus.value = res.data.message;
|
||||
if (res.data.status == 'ok') {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
updateStatus.value = err
|
||||
}).finally(() => {
|
||||
updatingDashboardLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
@@ -291,29 +291,32 @@ commonStore.getStartTime();
|
||||
<template>
|
||||
<v-app-bar elevation="0" height="55">
|
||||
|
||||
<v-btn v-if="useCustomizerStore().uiTheme==='PurpleTheme'" style="margin-left: 22px;" class="hidden-md-and-down text-secondary" color="lightsecondary" icon rounded="sm"
|
||||
variant="flat" @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||
<v-btn v-if="useCustomizerStore().uiTheme === 'PurpleTheme'" style="margin-left: 22px;"
|
||||
class="hidden-md-and-down text-secondary" color="lightsecondary" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else style="margin-left: 22px; color: var(--v-theme-primaryText); background-color: var(--v-theme-secondary)" class="hidden-md-and-down" icon rounded="sm"
|
||||
variant="flat" @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||
<v-btn v-else
|
||||
style="margin-left: 22px; color: var(--v-theme-primaryText); background-color: var(--v-theme-secondary)"
|
||||
class="hidden-md-and-down" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="hidden-lg-and-up ms-3" color="lightsecondary" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||
<v-btn v-if="useCustomizerStore().uiTheme === 'PurpleTheme'" class="hidden-lg-and-up ms-3" color="lightsecondary"
|
||||
icon rounded="sm" variant="flat" @click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else class="hidden-lg-and-up ms-3" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<div class="logo-container" :class="{'mobile-logo': $vuetify.display.xs}" @click="$router.push('/about')">
|
||||
<div class="logo-container" :class="{ 'mobile-logo': $vuetify.display.xs }" @click="$router.push('/about')">
|
||||
<span class="logo-text">Astr<span class="logo-text-light">Bot</span></span>
|
||||
<span class="version-text hidden-xs">{{ botCurrVersion }}</span>
|
||||
</div>
|
||||
|
||||
<v-spacer/>
|
||||
<v-spacer />
|
||||
|
||||
<!-- 版本提示信息 - 在手机上隐藏 -->
|
||||
<div class="mr-4 hidden-xs">
|
||||
@@ -329,20 +332,25 @@ commonStore.getStartTime();
|
||||
<LanguageSwitcher variant="header" />
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
<v-btn size="small" @click="toggleDarkMode();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm">
|
||||
<v-btn size="small" @click="toggleDarkMode();" class="action-btn" color="var(--v-theme-surface)" variant="flat"
|
||||
rounded="sm" icon>
|
||||
<v-icon v-if="useCustomizerStore().uiTheme === 'PurpleThemeDark'">mdi-weather-night</v-icon>
|
||||
<v-icon v-else>mdi-white-balance-sunny</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- 更新对话框 -->
|
||||
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1200'" :fullscreen="$vuetify.display.xs">
|
||||
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1200'"
|
||||
:fullscreen="$vuetify.display.xs">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
|
||||
<v-icon class="hidden-sm-and-up">mdi-update</v-icon>
|
||||
<span class="hidden-xs">{{ t('core.header.buttons.update') }}</span>
|
||||
</v-btn>
|
||||
<v-tooltip>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props" icon>
|
||||
<v-icon>mdi-arrow-up-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
{{ t('core.header.buttons.update') }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="mobile-card-title">
|
||||
@@ -361,8 +369,8 @@ commonStore.getStartTime();
|
||||
</div>
|
||||
|
||||
<div v-if="releaseMessage"
|
||||
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"
|
||||
v-html="md.render(releaseMessage)" class="markdown-content">
|
||||
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"
|
||||
v-html="md.render(releaseMessage)" class="markdown-content">
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-4">
|
||||
@@ -380,15 +388,13 @@ commonStore.getStartTime();
|
||||
<v-tabs-window-item key="0" v-show="tab == 0">
|
||||
<div class="mb-4">
|
||||
<small>{{ t('core.header.updateDialog.dockerTip') }} <a
|
||||
href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
|
||||
href="https://containrrr.dev/watchtower/usage-overview/">{{
|
||||
t('core.header.updateDialog.dockerTipLink')
|
||||
}}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="releases.some(item => isPreRelease(item['tag_name']))"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
>
|
||||
<v-alert v-if="releases.some(item => isPreRelease(item['tag_name']))" type="warning" variant="tonal"
|
||||
border="start">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-alert-circle-outline</v-icon>
|
||||
</template>
|
||||
@@ -406,13 +412,8 @@ commonStore.getStartTime();
|
||||
<template v-slot:item.tag_name="{ item }: { item: { tag_name: string } }">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ item.tag_name }}</span>
|
||||
<v-chip
|
||||
v-if="isPreRelease(item.tag_name)"
|
||||
size="x-small"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-chip v-if="isPreRelease(item.tag_name)" size="x-small" color="warning" variant="tonal"
|
||||
class="ml-2">
|
||||
{{ t('core.header.updateDialog.preRelease') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
@@ -420,7 +421,8 @@ commonStore.getStartTime();
|
||||
<template v-slot:item.body="{ item }: { item: { body: string } }">
|
||||
<v-tooltip :text="item.body">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="x-small">{{ t('core.header.updateDialog.table.view') }}</v-btn>
|
||||
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="x-small">{{
|
||||
t('core.header.updateDialog.table.view') }}</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
@@ -435,14 +437,12 @@ commonStore.getStartTime();
|
||||
<!-- 开发版 -->
|
||||
<v-tabs-window-item key="1" v-show="tab == 1">
|
||||
<div style="margin-top: 16px;">
|
||||
<v-data-table
|
||||
:headers="[
|
||||
{ title: t('core.header.updateDialog.table.sha'), key: 'sha' },
|
||||
{ title: t('core.header.updateDialog.table.date'), key: 'date' },
|
||||
{ title: t('core.header.updateDialog.table.message'), key: 'message' },
|
||||
{ title: t('core.header.updateDialog.table.actions'), key: 'switch' }
|
||||
]"
|
||||
:items="devCommits" item-key="sha">
|
||||
<v-data-table :headers="[
|
||||
{ title: t('core.header.updateDialog.table.sha'), key: 'sha' },
|
||||
{ title: t('core.header.updateDialog.table.date'), key: 'date' },
|
||||
{ title: t('core.header.updateDialog.table.message'), key: 'message' },
|
||||
{ title: t('core.header.updateDialog.table.actions'), key: 'switch' }
|
||||
]" :items="devCommits" item-key="sha">
|
||||
<template v-slot:item.switch="{ item }: { item: { sha: string } }">
|
||||
<v-btn @click="switchVersion(item.sha)" rounded="xl" variant="plain" color="primary">
|
||||
{{ t('core.header.updateDialog.table.switch') }}
|
||||
@@ -457,11 +457,12 @@ commonStore.getStartTime();
|
||||
<h3 class="mb-4">{{ t('core.header.updateDialog.manualInput.title') }}</h3>
|
||||
|
||||
<v-text-field :label="t('core.header.updateDialog.manualInput.placeholder')" v-model="version" required
|
||||
variant="outlined"></v-text-field>
|
||||
variant="outlined"></v-text-field>
|
||||
<div class="mb-4">
|
||||
<small>{{ t('core.header.updateDialog.manualInput.hint') }}</small>
|
||||
<br>
|
||||
<a href="https://github.com/Soulter/AstrBot/commits/master"><small>{{ t('core.header.updateDialog.manualInput.linkText') }}</small></a>
|
||||
<a href="https://github.com/Soulter/AstrBot/commits/master"><small>{{
|
||||
t('core.header.updateDialog.manualInput.linkText') }}</small></a>
|
||||
</div>
|
||||
<v-btn color="error" style="border-radius: 10px;" @click="switchVersion(version)">
|
||||
{{ t('core.header.updateDialog.manualInput.confirm') }}
|
||||
@@ -471,7 +472,8 @@ commonStore.getStartTime();
|
||||
<div style="margin-top: 16px;">
|
||||
<h3 class="mb-4">{{ t('core.header.updateDialog.dashboardUpdate.title') }}</h3>
|
||||
<div class="mb-4">
|
||||
<small>{{ t('core.header.updateDialog.dashboardUpdate.currentVersion') }} {{ dashboardCurrentVersion }}</small>
|
||||
<small>{{ t('core.header.updateDialog.dashboardUpdate.currentVersion') }} {{ dashboardCurrentVersion
|
||||
}}</small>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
@@ -486,7 +488,7 @@ commonStore.getStartTime();
|
||||
</div>
|
||||
|
||||
<v-btn color="primary" style="border-radius: 10px;" @click="updateDashboard()"
|
||||
:disabled="!dashboardHasNewVersion" :loading="updatingDashboardLoading">
|
||||
:disabled="!dashboardHasNewVersion" :loading="updatingDashboardLoading">
|
||||
{{ t('core.header.updateDialog.dashboardUpdate.downloadAndUpdate') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -504,9 +506,9 @@ commonStore.getStartTime();
|
||||
<!-- 账户对话框 -->
|
||||
<v-dialog v-model="dialog" persistent :max-width="$vuetify.display.xs ? '90%' : '500'">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" class="action-btn mr-4" color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
|
||||
<v-btn size="small" class="action-btn mr-4" color="var(--v-theme-surface)" variant="flat" rounded="sm"
|
||||
v-bind="props" icon>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
<span class="hidden-xs ml-1">{{ t('core.header.buttons.account') }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="account-dialog">
|
||||
@@ -514,105 +516,51 @@ commonStore.getStartTime();
|
||||
<div class="d-flex flex-column align-center mb-6">
|
||||
<logo :title="t('core.header.logoTitle')" :subtitle="t('core.header.accountDialog.title')"></logo>
|
||||
</div>
|
||||
<v-alert
|
||||
v-if="accountWarning"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-alert v-if="accountWarning" type="warning" variant="tonal" border="start" class="mb-4">
|
||||
<strong>{{ t('core.header.accountDialog.securityWarning') }}</strong>
|
||||
</v-alert>
|
||||
|
||||
<v-alert
|
||||
v-if="accountEditStatus.success"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-alert v-if="accountEditStatus.success" type="success" variant="tonal" border="start" class="mb-4">
|
||||
{{ accountEditStatus.message }}
|
||||
</v-alert>
|
||||
|
||||
<v-alert
|
||||
v-if="accountEditStatus.error"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-alert v-if="accountEditStatus.error" type="error" variant="tonal" border="start" class="mb-4">
|
||||
{{ accountEditStatus.message }}
|
||||
</v-alert>
|
||||
|
||||
<v-form v-model="formValid" @submit.prevent="accountEdit">
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:label="t('core.header.accountDialog.form.currentPassword')"
|
||||
variant="outlined"
|
||||
required
|
||||
clearable
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
<v-text-field v-model="password" :append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:type="showPassword ? 'text' : 'password'" :label="t('core.header.accountDialog.form.currentPassword')"
|
||||
variant="outlined" required clearable @click:append-inner="showPassword = !showPassword"
|
||||
prepend-inner-icon="mdi-lock-outline" hide-details="auto" class="mb-4"></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="newPassword"
|
||||
:append-inner-icon="showNewPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:type="showNewPassword ? 'text' : 'password'"
|
||||
:rules="passwordRules"
|
||||
:label="t('core.header.accountDialog.form.newPassword')"
|
||||
variant="outlined"
|
||||
required
|
||||
clearable
|
||||
@click:append-inner="showNewPassword = !showNewPassword"
|
||||
prepend-inner-icon="mdi-lock-plus-outline"
|
||||
:hint="t('core.header.accountDialog.form.passwordHint')"
|
||||
persistent-hint
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
<v-text-field v-model="newPassword" :append-inner-icon="showNewPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:type="showNewPassword ? 'text' : 'password'" :rules="passwordRules"
|
||||
:label="t('core.header.accountDialog.form.newPassword')" variant="outlined" required clearable
|
||||
@click:append-inner="showNewPassword = !showNewPassword" prepend-inner-icon="mdi-lock-plus-outline"
|
||||
:hint="t('core.header.accountDialog.form.passwordHint')" persistent-hint class="mb-4"></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="newUsername"
|
||||
:rules="usernameRules"
|
||||
:label="t('core.header.accountDialog.form.newUsername')"
|
||||
variant="outlined"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-account-edit-outline"
|
||||
:hint="t('core.header.accountDialog.form.usernameHint')"
|
||||
persistent-hint
|
||||
class="mb-3"
|
||||
></v-text-field>
|
||||
<v-text-field v-model="newUsername" :rules="usernameRules"
|
||||
:label="t('core.header.accountDialog.form.newUsername')" variant="outlined" clearable
|
||||
prepend-inner-icon="mdi-account-edit-outline" :hint="t('core.header.accountDialog.form.usernameHint')"
|
||||
persistent-hint class="mb-3"></v-text-field>
|
||||
</v-form>
|
||||
|
||||
|
||||
<div class="text-caption text-medium-emphasis mt-2">
|
||||
{{ t('core.header.accountDialog.form.defaultCredentials') }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="!accountWarning"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
@click="dialog = false"
|
||||
:disabled="accountEditStatus.loading"
|
||||
>
|
||||
<v-btn v-if="!accountWarning" variant="tonal" color="secondary" @click="dialog = false"
|
||||
:disabled="accountEditStatus.loading">
|
||||
{{ t('core.header.accountDialog.actions.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="accountEdit"
|
||||
:loading="accountEditStatus.loading"
|
||||
:disabled="!formValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
<v-btn color="primary" @click="accountEdit" :loading="accountEditStatus.loading" :disabled="!formValid"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ t('core.header.accountDialog.actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -665,9 +613,9 @@ commonStore.getStartTime();
|
||||
|
||||
/* 响应式布局样式 */
|
||||
.logo-container {
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -678,7 +626,7 @@ commonStore.getStartTime();
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
font-size: 24px;
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
@@ -687,7 +635,7 @@ commonStore.getStartTime();
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
|
||||
@@ -707,7 +655,7 @@ commonStore.getStartTime();
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
.action-btn {
|
||||
margin-right: 4px;
|
||||
min-width: 32px !important;
|
||||
@@ -717,11 +665,11 @@ commonStore.getStartTime();
|
||||
.v-card-title {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
|
||||
.v-card-text {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.v-tabs .v-tab {
|
||||
padding: 0 10px;
|
||||
font-size: 0.9rem;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import ChatPage from './ChatPage.vue';
|
||||
import Chat from '@/components/chat/Chat.vue'
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
const customizer = useCustomizerStore();
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@ const customizer = useCustomizerStore();
|
||||
<div
|
||||
style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div id="container">
|
||||
<ChatPage :chatbox-mode="true"></ChatPage>
|
||||
<Chat :chatbox-mode="true"></Chat>
|
||||
</div>
|
||||
</div>
|
||||
</v-app>
|
||||
@@ -18,24 +18,6 @@ const customizer = useCustomizerStore();
|
||||
<style scoped>
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#container {
|
||||
min-width: 600px;
|
||||
min-height: 370px;
|
||||
max-width: 1100px;
|
||||
max-height: 860px;
|
||||
padding: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
+11
-2044
File diff suppressed because it is too large
Load Diff
@@ -167,46 +167,12 @@
|
||||
<p class="text-disabled mt-2">{{ tm('status.emptyContent') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div v-else class="message-list">
|
||||
<div class="message-item" v-for="(msg, index) in conversationHistory" :key="index">
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.role === 'user'" class="user-message">
|
||||
<div class="message-bubble user-bubble">
|
||||
<span v-html="formatMessage(msg.content)"></span>
|
||||
|
||||
<!-- 图片附件 -->
|
||||
<div class="image-attachments" v-if="msg.image_url && msg.image_url.length > 0">
|
||||
<div v-for="(img, imgIndex) in msg.image_url" :key="imgIndex"
|
||||
class="image-attachment">
|
||||
<img :src="img" class="attached-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音频附件 -->
|
||||
<div class="audio-attachment" v-if="msg.audio_url">
|
||||
<audio controls class="audio-player">
|
||||
<source :src="msg.audio_url" type="audio/wav">
|
||||
{{ tm('status.audioNotSupported') }}
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<v-avatar class="user-avatar" color="deep-purple-lighten-3" size="36">
|
||||
<v-icon icon="mdi-account" />
|
||||
</v-avatar>
|
||||
</div>
|
||||
|
||||
<!-- 机器人消息 -->
|
||||
<div v-else class="bot-message">
|
||||
<v-avatar class="bot-avatar" color="deep-purple" size="36">
|
||||
<span class="text-h6">✨</span>
|
||||
</v-avatar>
|
||||
<div class="message-bubble bot-bubble">
|
||||
<div v-html="formatMessage(msg.content)" class="markdown-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 消息列表组件 -->
|
||||
<MessageList
|
||||
v-else
|
||||
:messages="formattedMessages"
|
||||
:isDark="false"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
@@ -291,6 +257,7 @@ import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import MessageList from '@/components/chat/MessageList.vue';
|
||||
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
@@ -303,7 +270,8 @@ const md = new MarkdownIt({
|
||||
export default {
|
||||
name: 'ConversationPage',
|
||||
components: {
|
||||
VueMonacoEditor
|
||||
VueMonacoEditor,
|
||||
MessageList
|
||||
},
|
||||
|
||||
setup() {
|
||||
@@ -484,6 +452,30 @@ export default {
|
||||
messageTypes: this.messageTypeFilter,
|
||||
search: this.search
|
||||
};
|
||||
},
|
||||
|
||||
// 将对话历史转换为 MessageList 组件期望的格式
|
||||
formattedMessages() {
|
||||
return this.conversationHistory.map(msg => {
|
||||
console.log('处理消息:', msg.role, msg.image_url, msg.audio_url);
|
||||
if (msg.role === 'user') {
|
||||
return {
|
||||
content: {
|
||||
type: 'user',
|
||||
message: this.extractTextFromContent(msg.content),
|
||||
image_url: this.extractImagesFromContent(msg.content),
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: {
|
||||
type: 'bot',
|
||||
message: this.extractTextFromContent(msg.content),
|
||||
embedded_images: this.extractImagesFromContent(msg.content),
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -861,6 +853,30 @@ export default {
|
||||
this.message = message;
|
||||
this.messageType = 'error';
|
||||
this.showMessage = true;
|
||||
},
|
||||
|
||||
// 从内容中提取文本
|
||||
extractTextFromContent(content) {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
} else if (Array.isArray(content)) {
|
||||
return content.filter(item => item.type === 'text')
|
||||
.map(item => item.text)
|
||||
.join('\n');
|
||||
} else if (typeof content === 'object') {
|
||||
return Object.values(content).filter(val => typeof val === 'string').join('');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
// 从内容中提取图片URL
|
||||
extractImagesFromContent(content) {
|
||||
if (Array.isArray(content)) {
|
||||
return content.filter(item => item.type === 'image_url')
|
||||
.map(item => item.image_url?.url)
|
||||
.filter(url => url);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -885,7 +901,7 @@ export default {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 聊天消息样式 */
|
||||
/* 聊天消息容器样式 */
|
||||
.conversation-messages-container {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
@@ -894,87 +910,6 @@ export default {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 8px;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
max-width: 80%;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
background-color: #f0f4ff;
|
||||
color: #333;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.bot-bubble {
|
||||
background-color: #fff;
|
||||
border: 1px solid #eaeaea;
|
||||
color: #333;
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
|
||||
.user-avatar,
|
||||
.bot-avatar {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 附件样式 */
|
||||
.image-attachments {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.attached-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.attached-image:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.audio-attachment {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
/* 对话详情卡片 */
|
||||
.conversation-detail-card {
|
||||
max-height: 90vh;
|
||||
@@ -982,95 +917,6 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Markdown内容样式 */
|
||||
.markdown-content {
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3,
|
||||
.markdown-content h4,
|
||||
.markdown-content h5,
|
||||
.markdown-content h6 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 1.8em;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-left: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: #f8f8f8;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: #f5f0ff;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #673ab7;
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #673ab7;
|
||||
padding-left: 16px;
|
||||
color: #666;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content th,
|
||||
.markdown-content td {
|
||||
border: 1px solid #eee;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background-color: #f5f0ff;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<!-- 人格卡片网格 -->
|
||||
<v-row>
|
||||
<v-col v-for="persona in personas" :key="persona.persona_id" cols="12" md="6" lg="4" xl="3">
|
||||
<v-card class="persona-card" elevation="2" rounded="lg" @click="viewPersona(persona)">
|
||||
<v-card class="persona-card" rounded="md" @click="viewPersona(persona)">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<div class="text-truncate ml-2">
|
||||
{{ persona.persona_id }}
|
||||
@@ -296,9 +296,9 @@
|
||||
<v-card-text>
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-2">{{ tm('form.systemPrompt') }}</h4>
|
||||
<div class="system-prompt-content">
|
||||
<pre class="system-prompt-content">
|
||||
{{ viewingPersona.system_prompt }}
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="viewingPersona.begin_dialogs && viewingPersona.begin_dialogs.length > 0" class="mb-4">
|
||||
@@ -759,10 +759,6 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.persona-card:hover {
|
||||
box-shadow: 0 8px 25px 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.system-prompt-preview {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
@@ -775,10 +771,10 @@ export default {
|
||||
}
|
||||
|
||||
.system-prompt-content {
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.3);
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">{{ t('title') }}</h1>
|
||||
<div class="dashboard-subtitle">{{ t('subtitle') }}</div>
|
||||
</div>
|
||||
|
||||
<v-slide-y-transition>
|
||||
<v-row v-if="noticeTitle && noticeContent" class="notice-row">
|
||||
<v-alert
|
||||
@@ -166,29 +161,10 @@ export default {
|
||||
background-color: var(--v-theme-background);
|
||||
min-height: calc(100vh - 64px);
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--v-theme-primaryText);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
|
||||
.notice-row {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-alert {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.1.5"
|
||||
version = "4.1.6"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user