Compare commits

...

13 Commits

Author SHA1 Message Date
Soulter 8f4a31cf8c chore: bump version to 4.1.7 2025-09-23 22:16:36 +08:00
Soulter 23549f13d6 Feature: 支持批量删除对话历史 (#2859)
* feat: 支持批量删除对话

closes: #2784

* feat: 添加加载状态禁用功能,优化用户交互体验
2025-09-23 22:10:56 +08:00
Soulter 869d11f9a6 perf: 优化验证配置时的性能,移除配置隐式类型转换
fixes: #2646
2025-09-23 21:04:14 +08:00
Soulter 02e73b82ee fix: 修复无法打开更新对话框的问题 2025-09-23 20:29:10 +08:00
Soulter f85f87f545 feat: WebChat 支持手动填写模型名
closes: #2830
2025-09-23 15:32:54 +08:00
Soulter 1fff5713f3 refactor: 解耦 PlatformPage 和 ProviderPage 的部分组件 2025-09-23 15:32:54 +08:00
Soulter 8453ec36f0 docs: Revise links for documentation and blog in README
Updated links in the README for documentation and blog.
2025-09-23 14:12:05 +08:00
Soulter d5b3ce8424 fix: update download_dashboard to log specific dashboard release URLs 2025-09-23 13:10:33 +08:00
Soulter 80cbbfa5ca chore: bump version to 4.1.6 2025-09-23 13:02:06 +08:00
Soulter 9177bb660f fix: improve error handling in run_agent for streaming responses 2025-09-23 10:34:24 +08:00
Soulter a3df39a01a perf: unified button styles
closes: #2748
2025-09-23 10:27:52 +08:00
Soulter 25dce05cbb refactor: improve webchat UI (#2853) 2025-09-23 10:19:26 +08:00
Soulter 1542ea3e03 fix: context.get_provider_by_id issue 2025-09-22 17:22:50 +08:00
29 changed files with 3341 additions and 3205 deletions
+2 -1
View File
@@ -18,7 +18,8 @@
<a href="https://github.com/Soulter/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/Soulter/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://astrbot.app/">查看文档</a>
<a href="https://astrbot.app/">文档</a>
<a href="https://blog.astrbot.app/">Blog</a>
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
</div>
+1 -1
View File
@@ -6,7 +6,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.1.5"
VERSION = "4.1.7"
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(
+19 -6
View File
@@ -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:
+7 -4
View File
@@ -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]:
+6 -18
View File
@@ -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)
+7 -24
View File
@@ -51,24 +51,6 @@ def validate_config(
def validate(data: dict, metadata: dict = schema, path=""):
for key, value in data.items():
if key not in metadata:
# 无 schema 的配置项,执行类型猜测
if isinstance(value, str):
try:
data[key] = int(value)
continue
except ValueError:
pass
try:
data[key] = float(value)
continue
except ValueError:
pass
if value.lower() == "true":
data[key] = True
elif value.lower() == "false":
data[key] = False
continue
meta = metadata[key]
if "type" not in meta:
@@ -127,12 +109,12 @@ def validate_config(
)
if is_core:
for key, group in schema.items():
group_meta = group.get("metadata")
if not group_meta:
continue
# logger.info(f"验证配置: 组 {key} ...")
validate(data, group_meta, path=f"{key}.")
meta_all = {
**schema["platform_group"]["metadata"],
**schema["provider_group"]["metadata"],
**schema["misc_config_group"]["metadata"],
}
validate(data, meta_all)
else:
validate(data, schema)
@@ -142,6 +124,7 @@ def validate_config(
def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False):
"""验证并保存配置"""
errors = None
logger.info(f"Saving config, is_core={is_core}")
try:
if is_core:
errors, post_config = validate_config(
+58 -8
View File
@@ -169,15 +169,65 @@ class ConversationRoute(Route):
"""删除对话"""
try:
data = await request.get_json()
user_id = data.get("user_id")
cid = data.get("cid")
if not user_id or not cid:
return Response().error("缺少必要参数: user_id 和 cid").__dict__
await self.core_lifecycle.conversation_manager.delete_conversation(
unified_msg_origin=user_id, conversation_id=cid
)
return Response().ok({"message": "对话删除成功"}).__dict__
# 检查是否是批量删除
if "conversations" in data:
# 批量删除
conversations = data.get("conversations", [])
if not conversations:
return (
Response().error("批量删除时conversations参数不能为空").__dict__
)
deleted_count = 0
failed_items = []
for conv in conversations:
user_id = conv.get("user_id")
cid = conv.get("cid")
if not user_id or not cid:
failed_items.append(
f"user_id:{user_id}, cid:{cid} - 缺少必要参数"
)
continue
try:
await self.core_lifecycle.conversation_manager.delete_conversation(
unified_msg_origin=user_id, conversation_id=cid
)
deleted_count += 1
except Exception as e:
failed_items.append(f"user_id:{user_id}, cid:{cid} - {str(e)}")
message = f"成功删除 {deleted_count} 个对话"
if failed_items:
message += f",失败 {len(failed_items)}"
return (
Response()
.ok(
{
"message": message,
"deleted_count": deleted_count,
"failed_count": len(failed_items),
"failed_items": failed_items,
}
)
.__dict__
)
else:
# 单个删除
user_id = data.get("user_id")
cid = data.get("cid")
if not user_id or not cid:
return Response().error("缺少必要参数: user_id 和 cid").__dict__
await self.core_lifecycle.conversation_manager.delete_conversation(
unified_msg_origin=user_id, conversation_id=cid
)
return Response().ok({"message": "对话删除成功"}).__dict__
except Exception as e:
logger.error(f"删除对话失败: {str(e)}\n{traceback.format_exc()}")
+3
View File
@@ -0,0 +1,3 @@
# What's Changed
1. fix: 修复在某些情况下,出现 「返回的 Provider 不是 Provider 类型的错误」
+8
View File
@@ -0,0 +1,8 @@
# What's Changed
1. perf: 优化 WebChat 等组件的 UI 风格
2. fix: 修复 4.1.6 版本可能无法点击更新按钮的问题
3. fix: 修复更新开发版的时候,可能无法同时更新 WebUI 的问题
4. feat: 支持在「对话数据」页批量删除对话
5. fix: 修复部分错误地显示「格式校验未通过」的问题
6. perf: WebChat 支持手动填写模型名称
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: 12px;"></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: 2px 16px;
border-radius: 12px;
}
.user-bubble {
color: var(--v-theme-primaryText);
padding: 12px 18px;
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: 6px;
}
/* 附件样式 */
.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,21 +1,11 @@
<template>
<div>
<!-- 选择提供商和模型按钮 -->
<v-btn
class="text-none"
variant="tonal"
rounded="xl"
size="small"
v-if="selectedProviderId && selectedModelName"
@click="showDialog = true">
<v-btn class="text-none" variant="tonal" rounded="xl" size="small"
v-if="selectedProviderId && selectedModelName" @click="openDialog">
{{ selectedProviderId }} / {{ selectedModelName }}
</v-btn>
<v-btn
variant="tonal"
rounded="xl"
size="small"
v-else
@click="showDialog = true">
<v-btn variant="tonal" rounded="xl" size="small" v-else @click="openDialog">
选择模型
</v-btn>
@@ -33,16 +23,12 @@
<h4>提供商</h4>
</div>
<v-list density="compact" nav class="provider-list">
<v-list-item
v-for="provider in providerConfigs"
:key="provider.id"
:value="provider.id"
@click="selectProvider(provider)"
:active="selectedProviderId === provider.id"
rounded="lg"
class="provider-item">
<v-list-item v-for="provider in providerConfigs" :key="provider.id" :value="provider.id"
@click="selectProvider(provider)" :active="tempSelectedProviderId === provider.id"
rounded="lg" class="provider-item">
<v-list-item-title>{{ provider.id }}</v-list-item-title>
<v-list-item-subtitle v-if="provider.api_base">{{ provider.api_base }}</v-list-item-subtitle>
<v-list-item-subtitle v-if="provider.api_base">{{ provider.api_base
}}</v-list-item-subtitle>
</v-list-item>
</v-list>
<div v-if="providerConfigs.length === 0" class="empty-state">
@@ -55,33 +41,28 @@
<div class="model-list-panel">
<div class="panel-header">
<h4>模型</h4>
<v-btn
v-if="selectedProviderId"
icon="mdi-refresh"
size="small"
variant="text"
@click="refreshModels"
:loading="loadingModels">
<v-btn v-if="tempSelectedProviderId" icon="mdi-refresh" size="small" variant="text"
@click="refreshModels" :loading="loadingModels">
</v-btn>
</div>
<v-list density="compact" nav class="model-list" v-if="selectedProviderId">
<v-list-item
v-for="model in modelList"
:key="model"
:value="model"
@click="selectModel(model)"
:active="selectedModelName === model"
rounded="lg"
<v-list density="compact" nav class="model-list" v-if="tempSelectedProviderId">
<v-text-field v-model="tempSelectedModelName" placeholder="自定义模型" hide-details solo variant="outlined" density="compact" class="mb-2 mx-2"></v-text-field>
<v-list-item v-for="model in modelList" :key="model" :value="model"
@click="selectModel(model)" :active="tempSelectedModelName === model" rounded="lg"
class="model-item">
<v-list-item-title>{{ model }}</v-list-item-title>
<v-list-item-subtitle v-if="model.description">{{ model.description }}</v-list-item-subtitle>
<v-list-item-subtitle v-if="model.description">{{ model.description
}}</v-list-item-subtitle>
</v-list-item>
</v-list>
<div v-else class="empty-state">
<v-icon icon="mdi-robot-outline" size="large" color="grey-lighten-1"></v-icon>
<div class="empty-text">请先选择提供商</div>
</div>
<div v-if="selectedProviderId && modelList.length === 0 && !loadingModels" class="empty-state">
<div v-if="tempSelectedProviderId && modelList.length === 0 && !loadingModels"
class="empty-state">
<v-icon icon="mdi-robot-off-outline" size="large" color="grey-lighten-1"></v-icon>
<div class="empty-text">该提供商暂无可用模型</div>
</div>
@@ -91,11 +72,8 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog" color="grey-darken-1">取消</v-btn>
<v-btn
text
@click="confirmSelection"
color="primary"
:disabled="!selectedProviderId || !selectedModelName">
<v-btn text @click="confirmSelection" color="primary"
:disabled="!tempSelectedProviderId || !tempSelectedModelName">
确认选择
</v-btn>
</v-card-actions>
@@ -127,12 +105,17 @@ export default {
modelList: [],
selectedProviderId: '',
selectedModelName: '',
// 临时选择状态,用于对话框内的选择
tempSelectedProviderId: '',
tempSelectedModelName: '',
loadingModels: false
};
},
mounted() {
// 从localStorage加载保存的选择
this.loadFromStorage();
// 初始化临时选择
this.resetTempSelection();
// 获取提供商列表
this.loadProviderConfigs();
// 如果有保存的选择,加载对应的模型列表
@@ -145,13 +128,13 @@ export default {
loadFromStorage() {
const savedProvider = localStorage.getItem('selectedProvider');
const savedModel = localStorage.getItem('selectedModel');
if (savedProvider) {
this.selectedProviderId = savedProvider;
} else if (this.initialProvider) {
this.selectedProviderId = this.initialProvider;
}
if (savedModel) {
this.selectedModelName = savedModel;
} else if (this.initialModel) {
@@ -215,36 +198,40 @@ export default {
// 选择提供商
selectProvider(provider) {
this.selectedProviderId = provider.id;
this.selectedModelName = ''; // 清空已选择的模型
this.tempSelectedProviderId = provider.id;
this.tempSelectedModelName = ''; // 清空已选择的模型
this.modelList = []; // 清空模型列表
this.getProviderModels(provider.id); // 获取该提供商的模型列表
},
// 选择模型
selectModel(model) {
this.selectedModelName = model;
this.tempSelectedModelName = model;
},
// 刷新模型列表
refreshModels() {
if (this.selectedProviderId) {
this.getProviderModels(this.selectedProviderId);
if (this.tempSelectedProviderId) {
this.getProviderModels(this.tempSelectedProviderId);
}
},
// 确认选择
confirmSelection() {
if (this.selectedProviderId && this.selectedModelName) {
if (this.tempSelectedProviderId && this.tempSelectedModelName) {
// 将临时选择应用到正式选择
this.selectedProviderId = this.tempSelectedProviderId;
this.selectedModelName = this.tempSelectedModelName;
// 保存到localStorage
this.saveToStorage();
// 触发事件通知父组件
this.$emit('selection-changed', {
providerId: this.selectedProviderId,
modelName: this.selectedModelName
});
this.closeDialog();
}
},
@@ -252,6 +239,24 @@ export default {
// 关闭对话框
closeDialog() {
this.showDialog = false;
// 重置临时选择为当前选择
this.resetTempSelection();
},
// 重置临时选择
resetTempSelection() {
this.tempSelectedProviderId = this.selectedProviderId;
this.tempSelectedModelName = this.selectedModelName;
// 如果有临时选择的提供商,重新加载模型列表
if (this.tempSelectedProviderId) {
this.getProviderModels(this.tempSelectedProviderId);
}
},
// 打开对话框
openDialog() {
this.resetTempSelection();
this.showDialog = true;
},
// 公开方法:获取当前选择
@@ -0,0 +1,169 @@
<template>
<v-dialog v-model="showDialog" max-width="900px" min-height="80%">
<v-card class="platform-selection-dialog" :title="tm('dialog.addPlatform')">
<v-card-text class="pa-4" style="overflow-y: auto;">
<v-row style="padding: 0px 8px;">
<v-col v-for="(template, name) in platformTemplates"
:key="name" cols="12" sm="6" md="6">
<v-card variant="outlined" hover class="platform-card" @click="selectTemplate(name)">
<div class="platform-card-content">
<div class="platform-card-text">
<v-card-title class="platform-card-title">{{ tm('dialog.connectTitle', { name }) }}</v-card-title>
<v-card-text class="text-caption text-medium-emphasis platform-card-description">
{{ getPlatformDescription(template, name) }}
</v-card-text>
</div>
<div class="platform-card-logo">
<img :src="getPlatformIcon(template.type)" v-if="getPlatformIcon(template.type)" class="platform-logo-img">
<div v-else class="platform-logo-fallback">
{{ name[0].toUpperCase() }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col
v-if="Object.keys(platformTemplates).length === 0"
cols="12">
<v-alert type="info" variant="tonal">
{{ tm('dialog.noTemplates') }}
</v-alert>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
import { getPlatformIcon, getPlatformDescription } from '@/utils/platformUtils';
export default {
name: 'AddNewPlatform',
emits: ['update:show', 'select-template'],
props: {
show: {
type: Boolean,
default: false
},
metadata: {
type: Object,
default: () => ({})
}
},
setup() {
const { tm } = useModuleI18n('features/platform');
return { tm };
},
computed: {
showDialog: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
}
},
platformTemplates() {
return this.metadata['platform_group']?.metadata?.platform?.config_template || {};
}
},
methods: {
// 从工具函数导入
getPlatformIcon,
getPlatformDescription,
selectTemplate(name) {
this.$emit('select-template', name);
this.closeDialog();
},
closeDialog() {
this.showDialog = false;
}
}
}
</script>
<style scoped>
.platform-selection-dialog .v-card-title {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.platform-card {
transition: all 0.3s ease;
height: 100%;
cursor: pointer;
overflow: hidden;
position: relative;
}
.platform-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
border-color: var(--v-primary-base);
}
.platform-card-content {
display: flex;
align-items: center;
height: 100px;
padding: 16px;
position: relative;
z-index: 2;
}
.platform-card-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.platform-card-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
padding: 0;
}
.platform-card-description {
padding: 0;
margin: 0;
}
.platform-card-logo {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.platform-logo-img {
max-width: 60px;
max-height: 60px;
opacity: 0.6;
object-fit: contain;
}
.platform-logo-fallback {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--v-primary-base);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
opacity: 0.3;
}
</style>
@@ -0,0 +1,239 @@
<template>
<v-dialog v-model="showDialog" max-width="1100px" min-height="95%">
<v-card :title="tm('dialogs.addProvider.title')">
<v-card-text style="overflow-y: auto;">
<v-tabs v-model="activeProviderTab" grow>
<v-tab value="chat_completion" class="font-weight-medium px-3">
<v-icon start>mdi-message-text</v-icon>
{{ tm('dialogs.addProvider.tabs.basic') }}
</v-tab>
<v-tab value="speech_to_text" class="font-weight-medium px-3">
<v-icon start>mdi-microphone-message</v-icon>
{{ tm('dialogs.addProvider.tabs.speechToText') }}
</v-tab>
<v-tab value="text_to_speech" class="font-weight-medium px-3">
<v-icon start>mdi-volume-high</v-icon>
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
</v-tab>
<v-tab value="embedding" class="font-weight-medium px-3">
<v-icon start>mdi-code-json</v-icon>
{{ tm('dialogs.addProvider.tabs.embedding') }}
</v-tab>
<v-tab value="rerank" class="font-weight-medium px-3">
<v-icon start>mdi-compare-vertical</v-icon>
{{ tm('dialogs.addProvider.tabs.rerank') }}
</v-tab>
</v-tabs>
<v-window v-model="activeProviderTab" class="mt-4">
<v-window-item
v-for="tabType in ['chat_completion', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
:key="tabType" :value="tabType">
<v-row class="mt-1">
<v-col v-for="(template, name) in getTemplatesByType(tabType)" :key="name" cols="12" sm="6"
md="4">
<v-card variant="outlined" hover class="provider-card"
@click="selectProviderTemplate(name)">
<div class="provider-card-content">
<div class="provider-card-text">
<v-card-title class="provider-card-title">接入 {{ name }}</v-card-title>
<v-card-text
class="text-caption text-medium-emphasis provider-card-description">
{{ getProviderDescription(template, name) }}
</v-card-text>
</div>
<div class="provider-card-logo">
<img :src="getProviderIcon(template.provider)"
v-if="getProviderIcon(template.provider)" class="provider-logo-img">
<div v-else class="provider-logo-fallback">
{{ name[0].toUpperCase() }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
<v-alert type="info" variant="tonal">
{{ tm('dialogs.addProvider.noTemplates', { type: getTabTypeName(tabType) }) }}
</v-alert>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog">
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
import { getProviderIcon, getProviderDescription } from '@/utils/providerUtils';
export default {
name: 'AddNewProvider',
props: {
show: {
type: Boolean,
default: false
},
metadata: {
type: Object,
default: () => ({})
}
},
emits: ['update:show', 'select-template'],
setup() {
const { tm } = useModuleI18n('features/provider');
return { tm };
},
data() {
return {
activeProviderTab: 'chat_completion'
};
},
computed: {
showDialog: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
}
},
// 翻译消息的计算属性
messages() {
return {
tabTypes: {
'chat_completion': this.tm('providers.tabs.chatCompletion'),
'speech_to_text': this.tm('providers.tabs.speechToText'),
'text_to_speech': this.tm('providers.tabs.textToSpeech'),
'embedding': this.tm('providers.tabs.embedding'),
'rerank': this.tm('providers.tabs.rerank')
}
};
}
},
methods: {
closeDialog() {
this.showDialog = false;
},
// 按提供商类型获取模板列表
getTemplatesByType(type) {
const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {};
const filtered = {};
for (const [name, template] of Object.entries(templates)) {
if (template.provider_type === type) {
filtered[name] = template;
}
}
return filtered;
},
// 从工具函数导入
getProviderIcon,
// 获取Tab类型的中文名称
getTabTypeName(tabType) {
return this.messages.tabTypes[tabType] || tabType;
},
// 获取提供商简介
getProviderDescription(template, name) {
return getProviderDescription(template, name, this.tm);
},
// 选择提供商模板
selectProviderTemplate(name) {
this.$emit('select-template', name);
this.closeDialog();
}
}
}
</script>
<style scoped>
.provider-card {
transition: all 0.3s ease;
height: 100%;
cursor: pointer;
overflow: hidden;
position: relative;
}
.provider-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
border-color: var(--v-primary-base);
}
.provider-card-content {
display: flex;
align-items: center;
height: 100px;
padding: 16px;
position: relative;
z-index: 2;
}
.provider-card-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.provider-card-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
padding: 0;
}
.provider-card-description {
padding: 0;
margin: 0;
}
.provider-card-logo {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.provider-logo-img {
width: 60px;
height: 60px;
opacity: 0.6;
object-fit: contain;
}
.provider-logo-fallback {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--v-primary-base);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
opacity: 0.3;
}
</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>
@@ -12,6 +12,13 @@
"title": "Conversation History",
"refresh": "Refresh"
},
"batch": {
"deleteSelected": "Delete Selected ({count})"
},
"pagination": {
"itemsPerPage": "Items per page",
"showingItems": "Showing {start}-{end} of {total} items"
},
"table": {
"headers": {
"title": "Conversation Title",
@@ -61,6 +68,13 @@
"message": "Are you sure you want to delete conversation {title}? This action cannot be undone.",
"cancel": "Cancel",
"confirm": "Delete"
},
"batchDelete": {
"title": "Batch Delete Confirmation",
"message": "Are you sure you want to delete the selected {count} conversations? This action cannot be undone, please proceed with caution!",
"andMore": "and {count} more",
"cancel": "Cancel",
"confirm": "Batch Delete"
}
},
"messages": {
@@ -72,6 +86,10 @@
"historyError": "Failed to fetch conversation history",
"historySaveSuccess": "Conversation history saved successfully",
"historySaveError": "Failed to save conversation history",
"invalidJson": "Invalid JSON format"
"invalidJson": "Invalid JSON format",
"noItemSelected": "Please select conversations to delete first",
"batchDeleteSuccess": "Successfully deleted {count} conversations",
"batchDeleteError": "Batch delete failed",
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed"
}
}
@@ -12,6 +12,13 @@
"title": "对话历史",
"refresh": "刷新"
},
"batch": {
"deleteSelected": "删除选中 ({count})"
},
"pagination": {
"itemsPerPage": "每页",
"showingItems": "显示 {start}-{end} 项,共 {total} 项"
},
"table": {
"headers": {
"title": "对话标题",
@@ -61,6 +68,13 @@
"message": "确定要删除对话 {title} 吗?此操作不可恢复。",
"cancel": "取消",
"confirm": "删除"
},
"batchDelete": {
"title": "批量删除确认",
"message": "确定要删除选中的 {count} 个对话吗?此操作不可恢复,请谨慎操作!",
"andMore": "等 {count} 个",
"cancel": "取消",
"confirm": "批量删除"
}
},
"messages": {
@@ -72,6 +86,10 @@
"historyError": "获取对话历史失败",
"historySaveSuccess": "对话历史保存成功",
"historySaveError": "对话历史保存失败",
"invalidJson": "JSON格式无效"
"invalidJson": "JSON格式无效",
"noItemSelected": "请先选择要删除的对话",
"batchDeleteSuccess": "成功删除 {count} 个对话",
"batchDeleteError": "批量删除失败",
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个"
}
}
@@ -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,19 +332,19 @@ 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>
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props" icon>
<v-icon>mdi-arrow-up-circle</v-icon>
</v-btn>
</template>
<v-card>
@@ -361,8 +364,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 +383,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 +407,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 +416,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 +432,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 +452,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 +467,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 +483,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 +501,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 +511,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 +608,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 +621,7 @@ commonStore.getStartTime();
}
.logo-text {
font-size: 24px;
font-size: 24px;
font-weight: 1000;
}
@@ -687,7 +630,7 @@ commonStore.getStartTime();
}
.version-text {
font-size: 12px;
font-size: 12px;
color: var(--v-theme-secondaryText);
}
@@ -707,7 +650,7 @@ commonStore.getStartTime();
.logo-text {
font-size: 20px;
}
.action-btn {
margin-right: 4px;
min-width: 32px !important;
@@ -717,11 +660,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;
+78
View File
@@ -0,0 +1,78 @@
/**
* 平台相关工具函数
*/
/**
* 获取平台图标
* @param {string} name - 平台名称或类型
* @returns {string|undefined} 图标URL
*/
export function getPlatformIcon(name) {
if (name === 'aiocqhttp' || name === 'qq_official' || name === 'qq_official_webhook') {
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
} else if (name === 'wecom') {
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
} else if (name === 'wechatpadpro' || name === 'weixin_official_account' || name === 'wechat') {
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
} else if (name === 'lark') {
return new URL('@/assets/images/platform_logos/lark.png', import.meta.url).href
} else if (name === 'dingtalk') {
return new URL('@/assets/images/platform_logos/dingtalk.svg', import.meta.url).href
} else if (name === 'telegram') {
return new URL('@/assets/images/platform_logos/telegram.svg', import.meta.url).href
} else if (name === 'discord') {
return new URL('@/assets/images/platform_logos/discord.svg', import.meta.url).href
} else if (name === 'slack') {
return new URL('@/assets/images/platform_logos/slack.svg', import.meta.url).href
} else if (name === 'kook') {
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
} else if (name === 'vocechat') {
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
} else if (name === 'satori' || name === 'Satori') {
return new URL('@/assets/images/platform_logos/satori.png', import.meta.url).href
} else if (name === 'misskey') {
return new URL('@/assets/images/platform_logos/misskey.png', import.meta.url).href
}
}
/**
* 获取平台教程链接
* @param {string} platformType - 平台类型
* @returns {string} 教程链接
*/
export function getTutorialLink(platformType) {
const tutorialMap = {
"qq_official_webhook": "https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html",
"qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html",
"aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html",
"wecom": "https://docs.astrbot.app/deploy/platform/wecom.html",
"lark": "https://docs.astrbot.app/deploy/platform/lark.html",
"telegram": "https://docs.astrbot.app/deploy/platform/telegram.html",
"dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html",
"wechatpadpro": "https://docs.astrbot.app/deploy/platform/wechat/wechatpadpro.html",
"weixin_official_account": "https://docs.astrbot.app/deploy/platform/weixin-official-account.html",
"discord": "https://docs.astrbot.app/deploy/platform/discord.html",
"slack": "https://docs.astrbot.app/deploy/platform/slack.html",
"kook": "https://docs.astrbot.app/deploy/platform/kook.html",
"vocechat": "https://docs.astrbot.app/deploy/platform/vocechat.html",
"satori": "https://docs.astrbot.app/deploy/platform/satori/llonebot.html",
"misskey": "https://docs.astrbot.app/deploy/platform/misskey.html",
}
return tutorialMap[platformType] || "https://docs.astrbot.app";
}
/**
* 获取平台描述
* @param {Object} template - 平台模板
* @param {string} name - 平台名称
* @returns {string} 平台描述
*/
export function getPlatformDescription(template, name) {
// special judge for community platforms
if (name.includes('vocechat')) {
return "由 @HikariFroya 提供。";
} else if (name.includes('kook')) {
return "由 @wuyan1003 提供。"
}
return '';
}
+51
View File
@@ -0,0 +1,51 @@
/**
* 提供商相关的工具函数
*/
/**
* 获取提供商类型对应的图标
* @param {string} type - 提供商类型
* @returns {string} 图标 URL
*/
export function getProviderIcon(type) {
const icons = {
'openai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
'azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
'xai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg',
'anthropic': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
'ppio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',
'dify': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dify-color.svg',
'dashscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/alibabacloud-color.svg',
'fastgpt': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fastgpt-color.svg',
'lm_studio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/lmstudio.svg',
'fishaudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg',
'minimax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
'302ai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.53.0/files/icons/ai302-color.svg',
'microsoft': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/microsoft.svg',
'vllm': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/vllm.svg',
};
return icons[type] || '';
}
/**
* 获取提供商简介
* @param {Object} template - 模板对象
* @param {string} name - 提供商名称
* @param {Function} tm - 翻译函数
* @returns {string} 提供商描述
*/
export function getProviderDescription(template, name, tm) {
if (name == 'OpenAI') {
return tm('providers.description.openai', { type: template.type });
} else if (name == 'vLLM Rerank') {
return tm('providers.description.vllm_rerank', { type: template.type });
}
return tm('providers.description.default', { type: template.type });
}
+3 -21
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+226 -300
View File
@@ -10,7 +10,7 @@
<v-col cols="12" sm="6" md="4">
<v-combobox v-model="platformFilter" :label="tm('filters.platform')"
:items="availablePlatforms" chips multiple clearable variant="solo-filled" flat
density="compact" hide-details>
density="compact" hide-details :disabled="loading">
<template v-slot:selection="{ item }">
<v-chip size="small" label>
{{ item.title }}
@@ -21,7 +21,8 @@
<v-col cols="12" sm="6" md="4">
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems"
chips multiple clearable variant="solo-filled" density="compact" hide-details flat>
chips multiple clearable variant="solo-filled" density="compact" hide-details flat
:disabled="loading">
<template v-slot:selection="{ item }">
<v-chip size="small" variant="solo-filled" label>
{{ item.title }}
@@ -33,22 +34,33 @@
<v-col cols="12" sm="12" md="4">
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify"
:label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat
clearable></v-text-field>
clearable :disabled="loading"></v-text-field>
</v-col>
</v-row>
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
:loading="loading" size="small">
:loading="loading" size="small" class="mr-2">
{{ tm('history.refresh') }}
</v-btn>
<v-btn
v-if="selectedItems.length > 0"
color="error"
prepend-icon="mdi-delete"
variant="tonal"
@click="confirmBatchDelete"
:disabled="loading"
size="small">
{{ tm('batch.deleteSelected', { count: selectedItems.length }) }}
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-0">
<v-data-table :headers="tableHeaders" :items="conversations" :loading="loading"
style="font-size: 12px;" density="comfortable" hide-default-footer items-per-page="10"
<v-data-table v-model="selectedItems" :headers="tableHeaders" :items="conversations"
:loading="loading" style="font-size: 12px;" density="comfortable" hide-default-footer
class="elevation-0" :items-per-page="pagination.page_size"
:items-per-page-options="[10, 20, 50, 100]" @update:options="handleTableOptions">
:items-per-page-options="pageSizeOptions" show-select return-object
:disabled="loading" @update:options="handleTableOptions">
<template v-slot:item.title="{ item }">
<div class="d-flex align-center">
<span>{{ item.title || tm('status.noTitle') }}</span>
@@ -82,15 +94,15 @@
<template v-slot:item.actions="{ item }">
<div class="actions-wrapper">
<v-btn icon variant="plain" size="x-small" class="action-button"
@click="viewConversation(item)">
@click="viewConversation(item)" :disabled="loading">
<v-icon>mdi-eye</v-icon>
</v-btn>
<v-btn icon variant="plain" size="x-small" class="action-button"
@click="editConversation(item)">
@click="editConversation(item)" :disabled="loading">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon color="error" variant="plain" size="x-small" class="action-button"
@click="confirmDeleteConversation(item)">
@click="confirmDeleteConversation(item)" :disabled="loading">
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
@@ -105,9 +117,25 @@
</v-data-table>
<!-- 分页控制 -->
<div class="d-flex justify-end">
<div class="d-flex justify-center py-3">
<!-- 每页大小选择器 -->
<div class="d-flex justify-between align-center px-4 py-2 bg-grey-lighten-5">
<div class="d-flex align-center">
<span class="text-caption mr-2">{{ tm('pagination.itemsPerPage') }}:</span>
<v-select v-model="pagination.page_size" :items="pageSizeOptions" variant="outlined"
density="compact" hide-details style="max-width: 100px;"
:disabled="loading" @update:model-value="onPageSizeChange"></v-select>
</div>
<div class="text-caption ml-4">
{{ tm('pagination.showingItems', {
start: Math.min((pagination.page - 1) * pagination.page_size + 1, pagination.total),
end: Math.min(pagination.page * pagination.page_size, pagination.total),
total: pagination.total
}) }}
</div>
</div>
<v-pagination v-model="pagination.page" :length="pagination.total_pages" :disabled="loading"
@update:model-value="fetchConversations" rounded="circle"></v-pagination>
@update:model-value="fetchConversations" rounded="circle" :total-visible="7"></v-pagination>
</div>
</v-card-text>
</v-card>
@@ -116,24 +144,20 @@
<!-- 对话详情对话框 -->
<v-dialog v-model="dialogView" max-width="900px" scrollable>
<v-card class="conversation-detail-card">
<v-card-title class="bg-primary text-white py-3 d-flex align-center">
<v-icon color="white" class="me-2">mdi-eye</v-icon>
<v-card-title class="ml-2 mt-2 d-flex align-center">
<span class="text-truncate">{{ selectedConversation?.title || tm('status.noTitle') }}</span>
<v-spacer></v-spacer>
<div class="d-flex align-center" v-if="selectedConversation?.sessionInfo">
<v-chip color="white" text-color="primary" size="small" class="mr-2">
<v-chip text-color="primary" size="small" class="mr-2" rounded="md">
{{ selectedConversation.sessionInfo.platform }}
</v-chip>
<v-chip color="white" text-color="secondary" size="small">
<v-chip text-color="secondary" size="small" rounded="md">
{{ getMessageTypeDisplay(selectedConversation.sessionInfo.messageType) }}
</v-chip>
</div>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="py-4">
<v-card-text>
<div class="mb-4 d-flex align-center">
<v-btn color="secondary" variant="tonal" size="small" class="mr-2"
@click="isEditingHistory = !isEditingHistory">
@@ -167,51 +191,11 @@
<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>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="closeHistoryDialog">
@@ -261,7 +245,7 @@
<v-card-text class="py-4">
<p>{{ tm('dialogs.delete.message', { title: selectedConversation?.title || tm('status.noTitle') })
}}</p>
}}</p>
</v-card-text>
<v-divider></v-divider>
@@ -278,6 +262,48 @@
</v-card>
</v-dialog>
<!-- 批量删除确认对话框 -->
<v-dialog v-model="dialogBatchDelete" max-width="600px">
<v-card>
<v-card-title class="bg-error text-white py-3">
<v-icon color="white" class="me-2">mdi-delete</v-icon>
<span>{{ tm('dialogs.batchDelete.title') }}</span>
</v-card-title>
<v-card-text class="py-4">
<p class="mb-3">{{ tm('dialogs.batchDelete.message', { count: selectedItems.length }) }}</p>
<!-- 显示前几个要删除的对话 -->
<div v-if="selectedItems.length > 0" class="mb-3">
<v-chip v-for="(item, index) in selectedItems.slice(0, 5)" :key="`${item.user_id}-${item.cid}`"
size="small" class="mr-1 mb-1" closable @click:close="removeFromSelection(item)"
:disabled="loading">
{{ item.title || tm('status.noTitle') }}
</v-chip>
<v-chip v-if="selectedItems.length > 5" size="small" class="mr-1 mb-1">
{{ tm('dialogs.batchDelete.andMore', { count: selectedItems.length - 5 }) }}
</v-chip>
</div>
<v-alert type="warning" variant="tonal" class="mb-3">
{{ tm('dialogs.batchDelete.warning') }}
</v-alert>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="dialogBatchDelete = false" :disabled="loading">
{{ tm('dialogs.batchDelete.cancel') }}
</v-btn>
<v-btn color="error" @click="batchDeleteConversations" :loading="loading">
{{ tm('dialogs.batchDelete.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 消息提示 -->
<v-snackbar :timeout="3000" elevation="24" :color="messageType" v-model="showMessage" location="top">
{{ message }}
@@ -291,6 +317,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 +330,8 @@ const md = new MarkdownIt({
export default {
name: 'ConversationPage',
components: {
VueMonacoEditor
VueMonacoEditor,
MessageList
},
setup() {
@@ -323,32 +351,13 @@ export default {
conversations: [],
search: '',
headers: [],
selectedItems: [], // 批量选择的项目
// 筛选条件
platformFilter: [],
messageTypeFilter: [],
lastAppliedFilters: null, // 记录上次应用的筛选条件
// 平台颜色映射
platformColors: {
'telegram': 'blue-lighten-1',
'qq_official': 'purple-lighten-1',
'qq_official_webhook': 'purple-lighten-2',
'aiocqhttp': 'deep-purple-lighten-1',
'lark': 'cyan-darken-1',
'wecom': 'green-darken-1',
'dingtalk': 'blue-darken-2',
'default': 'grey-lighten-1'
},
// 消息类型颜色映射
messageTypeColors: {
'GroupMessage': 'green',
'FriendMessage': 'blue',
'GuildMessage': 'purple',
'default': 'grey'
},
// 分页数据
pagination: {
page: 1,
@@ -356,11 +365,13 @@ export default {
total: 0,
total_pages: 0
},
pageSizeOptions: [10, 20, 50, 100], // 每页大小选项
// 对话框控制
dialogView: false,
dialogEdit: false,
dialogDelete: false,
dialogBatchDelete: false, // 批量删除对话框
// 选中的对话
selectedConversation: null,
@@ -372,11 +383,6 @@ export default {
cid: '',
title: ''
},
defaultItem: {
user_id: '',
cid: '',
title: ''
},
// 表单验证
valid: true,
@@ -463,17 +469,6 @@ export default {
];
},
// 筛选后的对话 - 现在只用于额外的客户端筛选(排除astrbot和webchat
filteredConversations() {
return this.conversations.filter(conv => {
// 排除 user_id 为 astrbot 或 platform 为 webchat 的对话
if (conv.user_id === 'astrbot' || conv.sessionInfo?.platform === 'webchat') {
return false;
}
return true;
});
},
// 当前的筛选条件对象
currentFilters() {
const platforms = this.platformFilter.map(item =>
@@ -484,6 +479,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),
}
};
}
});
}
},
@@ -798,6 +817,88 @@ export default {
}
} catch (error) {
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.deleteError'));
} finally {
this.loading = false;
this.selectedItems = this.selectedItems.filter(item =>
!(item.user_id === this.selectedConversation.user_id && item.cid === this.selectedConversation.cid)
);
this.selectedConversation = null;
}
},
// 处理页面大小变更
onPageSizeChange() {
this.pagination.page = 1; // 重置到第一页
this.fetchConversations();
},
// 确认批量删除
confirmBatchDelete() {
if (this.selectedItems.length === 0) {
this.showErrorMessage(this.tm('messages.noItemSelected'));
return;
}
this.dialogBatchDelete = true;
},
// 从选择中移除项目
removeFromSelection(item) {
const index = this.selectedItems.findIndex(selected =>
selected.user_id === item.user_id && selected.cid === item.cid
);
if (index !== -1) {
this.selectedItems.splice(index, 1);
}
},
// 批量删除对话
async batchDeleteConversations() {
if (this.selectedItems.length === 0) {
this.showErrorMessage(this.tm('messages.noItemSelected'));
return;
}
this.loading = true;
try {
// 准备批量删除的数据
const conversations = this.selectedItems.map(item => ({
user_id: item.user_id,
cid: item.cid
}));
const response = await axios.post('/api/conversation/delete', {
conversations: conversations
});
if (response.data.status === "ok") {
const result = response.data.data;
this.dialogBatchDelete = false;
this.selectedItems = []; // 清空选择
// 显示结果消息
if (result.failed_count > 0) {
this.showErrorMessage(
this.tm('messages.batchDeletePartial', {
deleted: result.deleted_count,
failed: result.failed_count
})
);
} else {
this.showSuccessMessage(
this.tm('messages.batchDeleteSuccess', {
count: result.deleted_count
})
);
}
// 刷新列表
this.fetchConversations();
} else {
this.showErrorMessage(response.data.message || this.tm('messages.batchDeleteError'));
}
} catch (error) {
console.error('批量删除对话出错:', error);
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.batchDeleteError'));
} finally {
this.loading = false;
}
@@ -820,35 +921,6 @@ export default {
}).format(date);
},
// 格式化消息内容
formatMessage(content) {
// content 可能是数组
// [{"type": "image_url", "image_url": {"url": url_or_base64}}, {"type": "text", "text": "text"}]
let final_content = content;
if (Array.isArray(content)) {
// 处理数组内容
final_content = content.map(item => {
if (item.type === 'image_url') {
return `<img src="${item.image_url.url}" alt="Image" />`;
} else if (item.type === 'text') {
return item.text;
}
return '';
}).join('\n');
} else if (typeof content === 'object') {
// 处理对象内容
final_content = Object.values(content).join('');
} else if (typeof content === 'string') {
// 处理字符串内容
final_content = content;
} else if (!final_content) return this.tm('status.emptyContent');
// 使用markdown-it处理,默认安全(html: false会禁用HTML标签)
return md.render(final_content);
},
// 显示成功消息
showSuccessMessage(message) {
this.message = message;
@@ -861,6 +933,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 +981,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 +990,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 +997,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 {
+5 -9
View File
@@ -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;
+23 -204
View File
@@ -10,7 +10,8 @@
{{ tm('subtitle') }}
</p>
</div>
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddPlatformDialog = true" rounded="xl" size="x-large">
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddPlatformDialog = true"
rounded="xl" size="x-large">
{{ tm('addAdapter') }}
</v-btn>
</v-row>
@@ -25,14 +26,9 @@
<v-row v-else>
<v-col v-for="(platform, index) in config_data.platform || []" :key="index" cols="12" md="6" lg="4" xl="3">
<item-card
:item="platform"
title-field="id"
enabled-field="enable"
:bglogo="getPlatformIcon(platform.type || platform.id)"
@toggle-enabled="platformStatusChange"
@delete="deletePlatform"
@edit="editPlatform">
<item-card :item="platform" title-field="id" enabled-field="enable"
:bglogo="getPlatformIcon(platform.type || platform.id)" @toggle-enabled="platformStatusChange"
@delete="deletePlatform" @edit="editPlatform">
</item-card>
</v-col>
</v-row>
@@ -61,59 +57,13 @@
</v-container>
<!-- 添加平台适配器对话框 -->
<v-dialog v-model="showAddPlatformDialog" max-width="900px" min-height="80%">
<v-card class="platform-selection-dialog">
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
<v-icon color="white" class="me-2">mdi-plus-circle</v-icon>
<span>{{ tm('dialog.addPlatform') }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="showAddPlatformDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-4" style="overflow-y: auto;">
<v-row class="mt-1">
<v-col v-for="(template, name) in metadata['platform_group']?.metadata?.platform?.config_template || {}"
:key="name" cols="12" sm="6" md="6">
<v-card variant="outlined" hover class="platform-card" @click="selectPlatformTemplate(name)">
<div class="platform-card-content">
<div class="platform-card-text">
<v-card-title class="platform-card-title">{{ tm('dialog.connectTitle', { name }) }}</v-card-title>
<v-card-text class="text-caption text-medium-emphasis platform-card-description">
{{ getPlatformDescription(template, name) }}
</v-card-text>
</div>
<div class="platform-card-logo">
<img :src="getPlatformIcon(template.type)" v-if="getPlatformIcon(template.type)" class="platform-logo-img">
<div v-else class="platform-logo-fallback">
{{ name[0].toUpperCase() }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col
v-if="Object.keys(metadata['platform_group']?.metadata?.platform?.config_template || {}).length === 0"
cols="12">
<v-alert type="info" variant="tonal">
{{ tm('dialog.noTemplates') }}
</v-alert>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="metadata"
@select-template="selectPlatformTemplate" />
<!-- 配置对话框 -->
<v-dialog v-model="showPlatformCfg" persistent width="900px" max-width="90%">
<v-card>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" class="me-2">{{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
<span>{{ updatingMode ? tm('dialog.edit') : tm('dialog.add') }} {{ newSelectedPlatformName }} {{
tm('dialog.adapter') }}</span>
</v-card-title>
<v-card
:title="updatingMode ? tm('dialog.edit') : tm('dialog.add') + ` ${newSelectedPlatformName} ` + tm('dialog.adapter')">
<v-card-text class="py-4">
<v-row>
<v-col cols="12">
@@ -164,7 +114,7 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
}}</v-btn>
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -177,7 +127,9 @@
</v-card-title>
<v-card-text class="py-4">
<p>{{ tm('dialog.securityWarning.aiocqhttpTokenMissing') }}</p>
<span><a href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7" target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
<span><a
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-spacer></v-spacer>
@@ -199,8 +151,10 @@ import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import ItemCard from '@/components/shared/ItemCard.vue';
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { getPlatformIcon, getTutorialLink } from '@/utils/platformUtils';
export default {
name: 'PlatformPage',
@@ -208,7 +162,8 @@ export default {
AstrBotConfig,
WaitingForRestart,
ConsoleDisplayer,
ItemCard
ItemCard,
AddNewPlatform
},
setup() {
const { t } = useI18n();
@@ -285,69 +240,14 @@ export default {
},
methods: {
// 从工具函数导入
getPlatformIcon,
openTutorial() {
const tutorialUrl = this.getTutorialLink(this.newSelectedPlatformConfig.type);
const tutorialUrl = getTutorialLink(this.newSelectedPlatformConfig.type);
window.open(tutorialUrl, '_blank');
},
getPlatformIcon(name) {
if (name === 'aiocqhttp' || name === 'qq_official' || name === 'qq_official_webhook') {
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
} else if (name === 'wecom') {
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
} else if (name === 'wechatpadpro' || name === 'weixin_official_account' || name === 'wechat') {
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
} else if (name === 'lark') {
return new URL('@/assets/images/platform_logos/lark.png', import.meta.url).href
} else if (name === 'dingtalk') {
return new URL('@/assets/images/platform_logos/dingtalk.svg', import.meta.url).href
} else if (name === 'telegram') {
return new URL('@/assets/images/platform_logos/telegram.svg', import.meta.url).href
} else if (name === 'discord') {
return new URL('@/assets/images/platform_logos/discord.svg', import.meta.url).href
} else if (name === 'slack') {
return new URL('@/assets/images/platform_logos/slack.svg', import.meta.url).href
} else if (name === 'kook') {
return new URL('@/assets/images/platform_logos/kook.png', import.meta.url).href
} else if (name === 'vocechat') {
return new URL('@/assets/images/platform_logos/vocechat.png', import.meta.url).href
} else if (name === 'satori' || name === 'Satori') {
return new URL('@/assets/images/platform_logos/satori.png', import.meta.url).href
} else if (name === 'misskey') {
return new URL('@/assets/images/platform_logos/misskey.png', import.meta.url).href
}
},
getTutorialLink(platform_type) {
let tutorial_map = {
"qq_official_webhook": "https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html",
"qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html",
"aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html",
"wecom": "https://docs.astrbot.app/deploy/platform/wecom.html",
"lark": "https://docs.astrbot.app/deploy/platform/lark.html",
"telegram": "https://docs.astrbot.app/deploy/platform/telegram.html",
"dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html",
"wechatpadpro": "https://docs.astrbot.app/deploy/platform/wechat/wechatpadpro.html",
"weixin_official_account": "https://docs.astrbot.app/deploy/platform/weixin-official-account.html",
"discord": "https://docs.astrbot.app/deploy/platform/discord.html",
"slack": "https://docs.astrbot.app/deploy/platform/slack.html",
"kook": "https://docs.astrbot.app/deploy/platform/kook.html",
"vocechat": "https://docs.astrbot.app/deploy/platform/vocechat.html",
"satori": "https://docs.astrbot.app/deploy/platform/satori/llonebot.html",
"misskey": "https://docs.astrbot.app/deploy/platform/misskey.html",
}
return tutorial_map[platform_type] || "https://docs.astrbot.app";
},
getPlatformDescription(template, name) {
// special judge for community platforms
if (name.includes('vocechat')) {
return "由 @HikariFroya 提供。";
} else if (name.includes('kook')) {
return "由 @wuyan1003 提供。"
}
},
getConfig() {
axios.get('/api/config/get').then((res) => {
this.config_data = res.data.data.config;
@@ -358,7 +258,7 @@ export default {
});
},
// 添加一个新方法来选择平台模板
// 选择平台模板
selectPlatformTemplate(name) {
this.newSelectedPlatformName = name;
this.showPlatformCfg = true;
@@ -366,7 +266,6 @@ export default {
this.newSelectedPlatformConfig = JSON.parse(JSON.stringify(
this.metadata['platform_group']?.metadata?.platform?.config_template[name] || {}
));
this.showAddPlatformDialog = false;
},
addFromDefaultConfigTmpl(index) {
@@ -483,7 +382,7 @@ export default {
this.oneBotEmptyTokenWarningResolve(continueWithWarning);
this.oneBotEmptyTokenWarningResolve = null;
}
if (!continueWithWarning) {
this.loading = false;
}
@@ -535,84 +434,4 @@ export default {
padding: 20px;
padding-top: 8px;
}
.platform-selection-dialog .v-card-title {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.platform-card {
transition: all 0.3s ease;
height: 100%;
cursor: pointer;
overflow: hidden;
position: relative;
}
.platform-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
border-color: var(--v-primary-base);
}
.platform-card-content {
display: flex;
align-items: center;
height: 100px;
padding: 16px;
position: relative;
z-index: 2;
}
.platform-card-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.platform-card-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
padding: 0;
}
.platform-card-description {
padding: 0;
margin: 0;
}
.platform-card-logo {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.platform-logo-img {
max-width: 60px;
max-height: 60px;
opacity: 0.6;
object-fit: contain;
}
.platform-logo-fallback {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--v-primary-base);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
opacity: 0.3;
}
</style>
+13 -215
View File
@@ -155,86 +155,15 @@
</v-container>
<!-- 添加提供商对话框 -->
<v-dialog v-model="showAddProviderDialog" max-width="1100px" min-height="95%">
<v-card class="provider-selection-dialog">
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
<v-icon color="white" class="me-2">mdi-plus-circle</v-icon>
<span>{{ tm('dialogs.addProvider.title') }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="showAddProviderDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-4" style="overflow-y: auto;">
<v-tabs v-model="activeProviderTab" grow slider-color="primary" bg-color="background">
<v-tab value="chat_completion" class="font-weight-medium px-3">
<v-icon start>mdi-message-text</v-icon>
{{ tm('dialogs.addProvider.tabs.basic') }}
</v-tab>
<v-tab value="speech_to_text" class="font-weight-medium px-3">
<v-icon start>mdi-microphone-message</v-icon>
{{ tm('dialogs.addProvider.tabs.speechToText') }}
</v-tab>
<v-tab value="text_to_speech" class="font-weight-medium px-3">
<v-icon start>mdi-volume-high</v-icon>
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
</v-tab>
<v-tab value="embedding" class="font-weight-medium px-3">
<v-icon start>mdi-code-json</v-icon>
{{ tm('dialogs.addProvider.tabs.embedding') }}
</v-tab>
<v-tab value="rerank" class="font-weight-medium px-3">
<v-icon start>mdi-compare-vertical</v-icon>
{{ tm('dialogs.addProvider.tabs.rerank') }}
</v-tab>
</v-tabs>
<v-window v-model="activeProviderTab" class="mt-4">
<v-window-item v-for="tabType in ['chat_completion', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
:key="tabType"
:value="tabType">
<v-row class="mt-1">
<v-col v-for="(template, name) in getTemplatesByType(tabType)"
:key="name"
cols="12" sm="6" md="4">
<v-card variant="outlined" hover class="provider-card" @click="selectProviderTemplate(name)">
<div class="provider-card-content">
<div class="provider-card-text">
<v-card-title class="provider-card-title">接入 {{ name }}</v-card-title>
<v-card-text class="text-caption text-medium-emphasis provider-card-description">
{{ getProviderDescription(template, name) }}
</v-card-text>
</div>
<div class="provider-card-logo">
<img :src="getProviderIcon(template.provider)" v-if="getProviderIcon(template.provider)" class="provider-logo-img">
<div v-else class="provider-logo-fallback">
{{ name[0].toUpperCase() }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
<v-alert type="info" variant="tonal">
{{ tm('dialogs.addProvider.noTemplates', { type: getTabTypeName(tabType) }) }}
</v-alert>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
</v-dialog>
<AddNewProvider
v-model:show="showAddProviderDialog"
:metadata="metadata"
@select-template="selectProviderTemplate"
/>
<!-- 配置对话框 -->
<v-dialog v-model="showProviderCfg" width="900" persistent>
<v-card>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" class="me-2">{{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
<span>{{ updatingMode ? tm('dialogs.config.editTitle') : tm('dialogs.config.addTitle') }} {{ newSelectedProviderName }} {{ tm('dialogs.config.provider') }}</span>
</v-card-title>
<v-card :title="updatingMode ? tm('dialogs.config.editTitle') : tm('dialogs.config.addTitle') + ` ${newSelectedProviderName} ` + tm('dialogs.config.provider')">
<v-card-text class="py-4">
<AstrBotConfig
:iterable="newSelectedProviderConfig"
@@ -309,7 +238,9 @@ import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import ItemCard from '@/components/shared/ItemCard.vue';
import AddNewProvider from '@/components/provider/AddNewProvider.vue';
import { useModuleI18n } from '@/i18n/composables';
import { getProviderIcon } from '@/utils/providerUtils';
export default {
name: 'ProviderPage',
@@ -317,7 +248,8 @@ export default {
AstrBotConfig,
WaitingForRestart,
ConsoleDisplayer,
ItemCard
ItemCard,
AddNewProvider
},
setup() {
const { tm } = useModuleI18n('features/provider');
@@ -360,7 +292,6 @@ export default {
// 新增提供商对话框相关
showAddProviderDialog: false,
activeProviderTab: 'chat_completion',
// 添加提供商类型分类
activeProviderTypeTab: 'all',
@@ -474,6 +405,9 @@ export default {
});
},
// 从工具函数导入
getProviderIcon,
// 获取空列表文本
getEmptyText() {
if (this.activeProviderTypeTab === 'all') {
@@ -483,63 +417,11 @@ export default {
}
},
// 按提供商类型获取模板列表
getTemplatesByType(type) {
const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {};
const filtered = {};
for (const [name, template] of Object.entries(templates)) {
if (template.provider_type === type) {
filtered[name] = template;
}
}
return filtered;
},
// 获取提供商类型对应的图标
getProviderIcon(type) {
const icons = {
'openai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
'azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
'xai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg',
'anthropic': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
'ppio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',
'dify': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dify-color.svg',
'dashscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/alibabacloud-color.svg',
'fastgpt': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fastgpt-color.svg',
'lm_studio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/lmstudio.svg',
'fishaudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg',
'minimax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
'302ai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.53.0/files/icons/ai302-color.svg',
'microsoft': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/microsoft.svg',
'vllm': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/vllm.svg',
};
return icons[type] || '';
},
// 获取Tab类型的中文名称
getTabTypeName(tabType) {
return this.messages.tabTypes[tabType] || tabType;
},
// 获取提供商简介
getProviderDescription(template, name) {
if (name == 'OpenAI') {
return this.tm('providers.description.openai', { type: template.type });
} else if (name == 'vLLM Rerank') {
return this.tm('providers.description.vllm_rerank', { type: template.type });
}
return this.tm('providers.description.default', { type: template.type });
},
// 选择提供商模板
selectProviderTemplate(name) {
this.newSelectedProviderName = name;
@@ -548,7 +430,6 @@ export default {
this.newSelectedProviderConfig = JSON.parse(JSON.stringify(
this.metadata['provider_group']?.metadata?.provider?.config_template[name] || {}
));
this.showAddProviderDialog = false;
},
configExistingProvider(provider) {
@@ -854,89 +735,6 @@ export default {
padding-top: 8px;
}
.provider-card {
transition: all 0.3s ease;
height: 100%;
cursor: pointer;
overflow: hidden;
position: relative;
}
.provider-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.05);
border-color: var(--v-primary-base);
}
.provider-card-content {
display: flex;
align-items: center;
height: 100px;
padding: 16px;
position: relative;
z-index: 2;
}
.provider-card-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.provider-card-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
padding: 0;
}
.provider-card-description {
padding: 0;
margin: 0;
}
.provider-card-logo {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.provider-logo-img {
width: 60px;
height: 60px;
opacity: 0.6;
object-fit: contain;
}
.provider-logo-fallback {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--v-primary-base);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
opacity: 0.3;
}
.v-tabs {
border-radius: 8px;
}
.v-window {
border-radius: 4px;
}
.status-card {
height: 120px;
overflow-y: auto;
@@ -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
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.1.5"
version = "4.1.7"
description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md"
requires-python = ">=3.10"