mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-08 05:50:12 +08:00
Compare commits
5 Commits
master
...
refactor-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad73b97afb | ||
|
|
d903ab1d44 | ||
|
|
2e57f86bc5 | ||
|
|
19962523ac | ||
|
|
99bca230e6 |
62
README.md
62
README.md
@@ -75,10 +75,6 @@ AstrBot is an open-source all-in-one Agent chatbot platform that integrates with
|
||||
|
||||
### One-Click Deployment
|
||||
|
||||
> [!NOTE]
|
||||
> Requires [uv](https://docs.astral.sh/uv/) to be installed.
|
||||
> For macOS users: due to macOS security checks, the first run of the `astrbot` command may take longer (about 10-20s).
|
||||
|
||||
For users who want to quickly experience AstrBot, are familiar with command-line usage, and can install a `uv` environment on their own, we recommend the `uv` one-click deployment method ⚡️:
|
||||
|
||||
```bash
|
||||
@@ -87,17 +83,20 @@ astrbot init # Only execute this command for the first time to initialize the en
|
||||
astrbot run
|
||||
```
|
||||
|
||||
> Requires [uv](https://docs.astral.sh/uv/) to be installed.
|
||||
> AstrBot requires Python 3.12 or later. The `--python 3.12` option ensures that `uv` creates the tool environment with Python 3.12.
|
||||
|
||||
> [!NOTE]
|
||||
> For macOS users: due to macOS security checks, the first run of the `astrbot` command may take longer (about 10-20s).
|
||||
|
||||
Update `astrbot`:
|
||||
|
||||
```bash
|
||||
uv tool upgrade astrbot --python 3.12
|
||||
```
|
||||
|
||||
### One-Click Cloud Deployment (RainYun)
|
||||
|
||||
For users who want one-click deployment and do not want to manage servers themselves, we recommend RainYun's one-click cloud deployment service ☁️:
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
> [!WARNING]
|
||||
> AstrBot deployed via `uv` **does not support upgrading through the WebUI**. To update, please run the command above from the command line.
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
@@ -105,6 +104,12 @@ For users familiar with containers and looking for a more stable, production-rea
|
||||
|
||||
Please refer to the official documentation: [Deploy AstrBot with Docker](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot).
|
||||
|
||||
### Deploy on RainYun
|
||||
|
||||
For users who want one-click deployment and do not want to manage servers themselves, we recommend RainYun's one-click cloud deployment service ☁️:
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
### Desktop Application Deployment
|
||||
|
||||
For users who want to use AstrBot on desktop and mainly use ChatUI, we recommend AstrBot App.
|
||||
@@ -137,18 +142,6 @@ yay -S astrbot-git
|
||||
|
||||
If you need panel-based management or deeper customization, see [BT-Panel Deployment](https://docs.astrbot.app/deploy/astrbot/btpanel.html) for BT Panel app-store setup, [1Panel Deployment](https://docs.astrbot.app/deploy/astrbot/1panel.html) for 1Panel app-market deployment, [CasaOS Deployment](https://docs.astrbot.app/deploy/astrbot/casaos.html) for NAS/home-server visual deployment, and [Manual Deployment](https://docs.astrbot.app/deploy/astrbot/cli.html) for fully custom source-based installation with `uv`.
|
||||
|
||||
|
||||
## ❤️ Sponsors
|
||||
|
||||
Welcome to sponsor us via [Afdian](https://afdian.com/a/astrbot_team) or [contact us](mailto:community@astrbot.app).
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://astrbot.app/#/sponsors">
|
||||
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
## Supported Messaging Platforms
|
||||
|
||||
Connect AstrBot to your favorite chat platform.
|
||||
@@ -212,6 +205,13 @@ Connect AstrBot to your favorite chat platform.
|
||||
| Xiaomi MiMo TTS | Text-to-Speech Services |
|
||||
| Volcano Engine TTS | Text-to-Speech Services |
|
||||
|
||||
## ❤️ Sponsors
|
||||
|
||||
<p align="center">
|
||||
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
|
||||
</p>
|
||||
|
||||
|
||||
## ❤️ Contributing
|
||||
|
||||
Issues and Pull Requests are always welcome! Feel free to submit your changes to this project :)
|
||||
@@ -235,7 +235,22 @@ pre-commit install
|
||||
|
||||
### QQ Groups
|
||||
|
||||
We have 15+ chat groups, please see: [Community](https://docs.astrbot.app/community.html) for details.
|
||||
- Group 1: 322154837 (Full)
|
||||
- Group 3: 630166526 (Full)
|
||||
- Group 4: 1077826412 (Full)
|
||||
- Group 5: 822130018 (Full)
|
||||
- Group 6: 753075035 (Full)
|
||||
- Group 7: 743746109 (Full)
|
||||
- Group 8: 1030353265 (Full)
|
||||
- Group 9: 1076659624 (Full)
|
||||
- Group 10: 1078079676 (Full)
|
||||
- Group 11: 704659519 (Full)
|
||||
- Group 12: 916228568 (Full)
|
||||
- Group 13: 1092185289
|
||||
- Group 14: 1103419483
|
||||
|
||||
- Developer Group(Chit-chat): 975206796
|
||||
- Developer Group(Formal): 1039761811
|
||||
|
||||
### Discord Server
|
||||
|
||||
@@ -249,10 +264,9 @@ Special thanks to all Contributors and plugin developers for their contributions
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=300&columns=15" />
|
||||
</a>
|
||||
|
||||
Open Source Friends ❤️
|
||||
Additionally, the birth of this project would not have been possible without the help of the following open-source projects:
|
||||
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - The amazing cat framework
|
||||
- [Mai-with-u/MaiBot](https://github.com/Mai-with-u/MaiBot) - The powerful "digital life" in your QQ!
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
|
||||
@@ -369,12 +369,6 @@ class AuthService:
|
||||
if not new_pwd and not new_username:
|
||||
return self.error("新用户名和新密码不能同时为空")
|
||||
|
||||
username_to_save = None
|
||||
if new_username is not None and new_username != "":
|
||||
if not isinstance(new_username, str) or len(new_username.strip()) < 3:
|
||||
return self.error("用户名长度至少3位")
|
||||
username_to_save = new_username.strip()
|
||||
|
||||
if new_pwd:
|
||||
if not isinstance(new_pwd, str):
|
||||
return self.error("新密码无效")
|
||||
@@ -390,8 +384,8 @@ class AuthService:
|
||||
await set_password_change_required(self.db, self.config, False)
|
||||
if is_totp_enabled(self.config):
|
||||
await revoke_user_trusted_devices(self.db)
|
||||
if username_to_save:
|
||||
self.config["dashboard"]["username"] = username_to_save
|
||||
if new_username:
|
||||
self.config["dashboard"]["username"] = new_username
|
||||
|
||||
self.config.save_config()
|
||||
|
||||
|
||||
@@ -160,6 +160,10 @@
|
||||
content: "\F0B79";
|
||||
}
|
||||
|
||||
.mdi-chat-outline::before {
|
||||
content: "\F0EDE";
|
||||
}
|
||||
|
||||
.mdi-chat-processing::before {
|
||||
content: "\F0B7B";
|
||||
}
|
||||
@@ -404,10 +408,6 @@
|
||||
content: "\F0209";
|
||||
}
|
||||
|
||||
.mdi-eye-outline::before {
|
||||
content: "\F06D0";
|
||||
}
|
||||
|
||||
.mdi-eyedropper::before {
|
||||
content: "\F020A";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,11 @@
|
||||
<div
|
||||
v-if="props.active"
|
||||
class="chat-ui"
|
||||
:class="{ 'is-dark': isDark, 'sidebar-collapsed': isSidebarCollapsed }"
|
||||
:class="{
|
||||
'is-dark': isDark,
|
||||
'sidebar-collapsed': isSidebarCollapsed,
|
||||
'dashboard-layout': !props.chatboxMode,
|
||||
}"
|
||||
>
|
||||
<v-navigation-drawer
|
||||
v-model="chatSidebarDrawer"
|
||||
@@ -169,160 +173,6 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<StyledMenu
|
||||
location="top start"
|
||||
offset="10"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
class="settings-btn"
|
||||
:class="{ 'icon-only': isSidebarCollapsed }"
|
||||
variant="text"
|
||||
:icon="isSidebarCollapsed"
|
||||
>
|
||||
<Settings
|
||||
:size="20"
|
||||
:class="['sidebar-action-icon', { 'mr-2': !isSidebarCollapsed }]"
|
||||
/>
|
||||
<span v-if="!isSidebarCollapsed">{{
|
||||
t("core.common.settings")
|
||||
}}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<div class="settings-menu-content">
|
||||
<v-menu
|
||||
location="end"
|
||||
offset="8"
|
||||
open-on-hover
|
||||
:close-on-content-click="true"
|
||||
>
|
||||
<template #activator="{ props: transportMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="transportMenuProps"
|
||||
class="styled-menu-item settings-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<template #prepend>
|
||||
<Cable :size="18" class="styled-menu-lucide-icon" />
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
tm("transport.title")
|
||||
}}</v-list-item-title>
|
||||
<template #append>
|
||||
<span class="settings-menu-value">{{
|
||||
currentTransportLabel
|
||||
}}</span>
|
||||
<ChevronRight :size="18" class="styled-menu-lucide-icon" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card class="styled-menu-card" elevation="8" rounded="lg">
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<v-list-item
|
||||
v-for="item in transportOptions"
|
||||
:key="item.value"
|
||||
class="styled-menu-item"
|
||||
:class="{
|
||||
'styled-menu-item-active': transportMode === item.value,
|
||||
}"
|
||||
rounded="md"
|
||||
@click="transportMode = item.value"
|
||||
>
|
||||
<v-list-item-title>{{
|
||||
tm(item.labelKey)
|
||||
}}</v-list-item-title>
|
||||
<template #append>
|
||||
<Check
|
||||
v-if="transportMode === item.value"
|
||||
:size="18"
|
||||
class="styled-menu-lucide-icon"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<v-menu
|
||||
location="end"
|
||||
offset="8"
|
||||
open-on-hover
|
||||
:close-on-content-click="true"
|
||||
>
|
||||
<template #activator="{ props: languageMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="languageMenuProps"
|
||||
class="styled-menu-item settings-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<template #prepend>
|
||||
<Languages :size="18" class="styled-menu-lucide-icon" />
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
t("core.common.language")
|
||||
}}</v-list-item-title>
|
||||
<template #append>
|
||||
<span class="settings-menu-value">{{
|
||||
currentLanguage?.label || locale
|
||||
}}</span>
|
||||
<ChevronRight :size="18" class="styled-menu-lucide-icon" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card class="styled-menu-card" elevation="8" rounded="lg">
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<v-list-item
|
||||
v-for="lang in languageOptions"
|
||||
:key="lang.value"
|
||||
class="styled-menu-item"
|
||||
:class="{
|
||||
'styled-menu-item-active': locale === lang.value,
|
||||
}"
|
||||
rounded="md"
|
||||
@click="switchLanguage(lang.value as Locale)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="language-flag">{{ lang.flag }}</span>
|
||||
</template>
|
||||
<v-list-item-title>{{ lang.label }}</v-list-item-title>
|
||||
<template #append>
|
||||
<Check
|
||||
v-if="locale === lang.value"
|
||||
:size="18"
|
||||
class="styled-menu-lucide-icon"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<v-list-item
|
||||
class="styled-menu-item settings-menu-item"
|
||||
rounded="md"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<template #prepend>
|
||||
<Sun
|
||||
v-if="isDark"
|
||||
:size="18"
|
||||
class="styled-menu-lucide-icon"
|
||||
/>
|
||||
<Moon v-else :size="18" class="styled-menu-lucide-icon" />
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
isDark ? tm("modes.lightMode") : tm("modes.darkMode")
|
||||
}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</StyledMenu>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<main
|
||||
@@ -551,20 +401,12 @@ import { useDisplay } from "vuetify";
|
||||
import { isAxiosError } from "axios";
|
||||
import {
|
||||
Box,
|
||||
Cable,
|
||||
Check,
|
||||
ChevronRight,
|
||||
Languages,
|
||||
Moon,
|
||||
PanelLeft,
|
||||
Pencil,
|
||||
Settings,
|
||||
SquarePen,
|
||||
Sun,
|
||||
Trash2,
|
||||
} from "@lucide/vue";
|
||||
import { chatApi, providerApi } from "@/api/v1";
|
||||
import StyledMenu from "@/components/shared/StyledMenu.vue";
|
||||
import ProjectDialog, {
|
||||
type ProjectFormData,
|
||||
} from "@/components/chat/ProjectDialog.vue";
|
||||
@@ -592,12 +434,7 @@ import { useProjects } from "@/composables/useProjects";
|
||||
import { useChatHeaderStore } from "@/stores/chatHeader";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import ProviderChatCompletionPanel from "@/components/provider/ProviderChatCompletionPanel.vue";
|
||||
import {
|
||||
useI18n,
|
||||
useLanguageSwitcher,
|
||||
useModuleI18n,
|
||||
} from "@/i18n/composables";
|
||||
import type { Locale } from "@/i18n/types";
|
||||
import { useI18n, useModuleI18n } from "@/i18n/composables";
|
||||
import { askForConfirmation, useConfirmDialog } from "@/utils/confirmDialog";
|
||||
import {
|
||||
contextLimit,
|
||||
@@ -621,8 +458,6 @@ const { t } = useI18n();
|
||||
const { tm } = useModuleI18n("features/chat");
|
||||
const confirmDialog = useConfirmDialog();
|
||||
const toast = useToast();
|
||||
const { languageOptions, currentLanguage, switchLanguage, locale } =
|
||||
useLanguageSwitcher();
|
||||
const {
|
||||
sessions,
|
||||
currSessionId,
|
||||
@@ -789,20 +624,6 @@ const transportMode = ref<TransportMode>(
|
||||
? "websocket"
|
||||
: "sse",
|
||||
);
|
||||
const transportOptions: Array<{ value: TransportMode; labelKey: string }> = [
|
||||
{ value: "sse", labelKey: "transport.sse" },
|
||||
{ value: "websocket", labelKey: "transport.websocket" },
|
||||
];
|
||||
const currentTransportLabel = computed(() =>
|
||||
tm(
|
||||
transportOptions.find((item) => item.value === transportMode.value)
|
||||
?.labelKey || "transport.sse",
|
||||
),
|
||||
);
|
||||
|
||||
watch(transportMode, (mode) => {
|
||||
localStorage.setItem("chat.transportMode", mode);
|
||||
});
|
||||
|
||||
const isDark = computed(() => customizer.uiTheme === "PurpleThemeDark");
|
||||
const canSend = computed(
|
||||
@@ -1657,9 +1478,6 @@ async function stopCurrentSession() {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
customizer.SET_UI_THEME(isDark.value ? "PurpleTheme" : "PurpleThemeDark");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1701,6 +1519,10 @@ function toggleTheme() {
|
||||
--chat-section-label: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.chat-ui.dashboard-layout .chat-main {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.chat-sidebar {
|
||||
top: 0 !important;
|
||||
height: 100vh !important;
|
||||
@@ -1853,8 +1675,7 @@ function toggleTheme() {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.new-chat-btn,
|
||||
.settings-btn {
|
||||
.new-chat-btn {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -1869,8 +1690,7 @@ function toggleTheme() {
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
|
||||
.new-chat-btn,
|
||||
.settings-btn {
|
||||
.new-chat-btn {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
height: 36px;
|
||||
@@ -1886,13 +1706,11 @@ function toggleTheme() {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.new-chat-btn:not(.icon-only),
|
||||
.settings-btn:not(.icon-only) {
|
||||
.new-chat-btn:not(.icon-only) {
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.new-chat-btn.icon-only,
|
||||
.settings-btn.icon-only {
|
||||
.new-chat-btn.icon-only {
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
min-width: 36px !important;
|
||||
@@ -1901,27 +1719,19 @@ function toggleTheme() {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-sidebar.collapsed .new-chat-btn.icon-only :deep(.v-btn__content),
|
||||
.chat-sidebar.collapsed .settings-btn.icon-only :deep(.v-btn__content) {
|
||||
.chat-sidebar.collapsed .new-chat-btn.icon-only :deep(.v-btn__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.new-chat-btn :deep(.v-btn__content),
|
||||
.settings-btn :deep(.v-btn__content) {
|
||||
.new-chat-btn :deep(.v-btn__content) {
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.chat-sidebar.collapsed .sidebar-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.new-chat-btn:hover,
|
||||
.settings-btn:hover {
|
||||
.new-chat-btn:hover {
|
||||
background: var(--chat-session-active-bg);
|
||||
}
|
||||
|
||||
@@ -2038,72 +1848,6 @@ function toggleTheme() {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
margin-top: auto;
|
||||
padding: 10px 16px 14px;
|
||||
}
|
||||
|
||||
.chat-sidebar.collapsed .sidebar-footer {
|
||||
width: 56px;
|
||||
box-sizing: border-box;
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.settings-menu-content {
|
||||
min-width: 270px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.settings-menu-item {
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.settings-menu-content :deep(.settings-menu-item .v-list-item__prepend) {
|
||||
width: 28px;
|
||||
margin-inline-end: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.settings-menu-content :deep(.settings-menu-item .v-list-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-menu-content :deep(.settings-menu-item .v-list-item-title) {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-menu-content :deep(.settings-menu-item .v-list-item__append) {
|
||||
margin-inline-start: auto;
|
||||
padding-inline-start: 18px;
|
||||
gap: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.styled-menu-lucide-icon {
|
||||
flex: 0 0 auto;
|
||||
color: currentcolor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.settings-menu-value {
|
||||
color: var(--chat-muted);
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
max-width: 92px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.language-flag {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -2307,4 +2051,9 @@ kbd {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-ui.dashboard-layout .chat-sidebar {
|
||||
top: 0 !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -180,7 +180,7 @@ const { success: toastSuccess, error: toastError } = useToast();
|
||||
|
||||
const variant = computed(() => props.variant);
|
||||
const menuLocation = computed(() =>
|
||||
props.variant === "header" ? "bottom start" : "top",
|
||||
props.variant === "header" ? "bottom end" : "top",
|
||||
);
|
||||
|
||||
const selectedProvider = computed(() =>
|
||||
|
||||
@@ -45,7 +45,7 @@ localStorage.setItem('uiTheme', uiTheme);
|
||||
const config: ConfigProps = {
|
||||
Sidebar_drawer: true,
|
||||
Customizer_drawer: false,
|
||||
mini_sidebar: false,
|
||||
mini_sidebar: true,
|
||||
fontTheme: 'Roboto',
|
||||
uiTheme,
|
||||
themeMode,
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
},
|
||||
"openapi": {
|
||||
"title": "OpenAPI"
|
||||
},
|
||||
"resources": {
|
||||
"title": "About"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"changelog": {
|
||||
"subtitle": "Review changes in the current and previous versions."
|
||||
},
|
||||
"documentation": {
|
||||
"subtitle": "Open the official AstrBot documentation."
|
||||
},
|
||||
"faq": {
|
||||
"subtitle": "Read common questions and troubleshooting notes."
|
||||
},
|
||||
"github": {
|
||||
"subtitle": "Visit the AstrBot GitHub repository."
|
||||
}
|
||||
},
|
||||
"systemConfig": {
|
||||
@@ -58,6 +75,12 @@
|
||||
},
|
||||
"network": {
|
||||
"title": "Network",
|
||||
"transport": {
|
||||
"title": "Chat Transport Mode",
|
||||
"subtitle": "Choose the connection method Chat uses for streaming messages. Stored locally in this browser.",
|
||||
"sse": "SSE",
|
||||
"websocket": "WebSocket"
|
||||
},
|
||||
"githubProxy": {
|
||||
"title": "GitHub Proxy Address",
|
||||
"subtitle": "Set the GitHub proxy address used when downloading plugins or updating AstrBot. This is effective in mainland China's network environment. Can be customized, input takes effect in real time. All addresses do not guarantee stability. If errors occur when updating plugins/projects, please first check if the proxy address is working properly.",
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
},
|
||||
"openapi": {
|
||||
"title": "OpenAPI"
|
||||
},
|
||||
"resources": {
|
||||
"title": "О программе"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"changelog": {
|
||||
"subtitle": "Просмотреть изменения текущей и предыдущих версий."
|
||||
},
|
||||
"documentation": {
|
||||
"subtitle": "Открыть официальную документацию AstrBot."
|
||||
},
|
||||
"faq": {
|
||||
"subtitle": "Посмотреть частые вопросы и советы по устранению проблем."
|
||||
},
|
||||
"github": {
|
||||
"subtitle": "Открыть репозиторий AstrBot на GitHub."
|
||||
}
|
||||
},
|
||||
"systemConfig": {
|
||||
@@ -58,6 +75,12 @@
|
||||
},
|
||||
"network": {
|
||||
"title": "Сеть",
|
||||
"transport": {
|
||||
"title": "Транспорт чата",
|
||||
"subtitle": "Выберите способ подключения, который чат использует для потоковой передачи сообщений. Настройка хранится локально в браузере.",
|
||||
"sse": "SSE",
|
||||
"websocket": "WebSocket"
|
||||
},
|
||||
"githubProxy": {
|
||||
"title": "Зеркало GitHub",
|
||||
"subtitle": "Адрес для ускорения загрузки плагинов и обновлений AstrBot. Особенно актуально для пользователей из Китая. Все адреса предоставляются как есть, если обновление не удается — проверьте доступность выбранного зеркала.",
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
},
|
||||
"openapi": {
|
||||
"title": "OpenAPI"
|
||||
},
|
||||
"resources": {
|
||||
"title": "关于"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"changelog": {
|
||||
"subtitle": "查看当前版本和历史版本的更新内容。"
|
||||
},
|
||||
"documentation": {
|
||||
"subtitle": "打开 AstrBot 官方文档。"
|
||||
},
|
||||
"faq": {
|
||||
"subtitle": "查看常见问题与排障说明。"
|
||||
},
|
||||
"github": {
|
||||
"subtitle": "访问 AstrBot GitHub 仓库。"
|
||||
}
|
||||
},
|
||||
"systemConfig": {
|
||||
@@ -58,6 +75,12 @@
|
||||
},
|
||||
"network": {
|
||||
"title": "网络",
|
||||
"transport": {
|
||||
"title": "通信传输模式",
|
||||
"subtitle": "选择聊天消息流式传输时使用的连接方式,设置保存在浏览器本地。",
|
||||
"sse": "SSE",
|
||||
"websocket": "WebSocket"
|
||||
},
|
||||
"githubProxy": {
|
||||
"title": "GitHub 加速地址",
|
||||
"subtitle": "设置下载插件或者更新 AstrBot 时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效。所有地址均不保证稳定性,如果在更新插件/项目时出现报错,请首先检查加速地址是否能正常使用。",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView, useRoute } from "vue-router";
|
||||
import { ref, onMounted, computed, watch } from "vue";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import VerticalSidebarVue from "./vertical-sidebar/VerticalSidebar.vue";
|
||||
import VerticalHeaderVue from "./vertical-header/VerticalHeader.vue";
|
||||
import ReadmeDialog from "@/components/shared/ReadmeDialog.vue";
|
||||
import Chat from "@/components/chat/Chat.vue";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useRouterLoadingStore } from "@/stores/routerLoading";
|
||||
import { useCommonStore } from "@/stores/common";
|
||||
@@ -18,27 +17,13 @@ const commonStore = useCommonStore();
|
||||
const { locale } = useI18n();
|
||||
const route = useRoute();
|
||||
const routerLoadingStore = useRouterLoadingStore();
|
||||
const isCurrentChatRoute = computed(
|
||||
() => route.path === "/chat" || route.path.startsWith("/chat/"),
|
||||
);
|
||||
const isPluginPageRoute = computed(
|
||||
() => route.path.startsWith("/plugin-page/"),
|
||||
);
|
||||
const isFullScreenRoute = computed(
|
||||
() => isCurrentChatRoute.value || isPluginPageRoute.value,
|
||||
);
|
||||
const shouldMountChat = ref(isCurrentChatRoute.value);
|
||||
|
||||
const showSidebar = computed(() => !isCurrentChatRoute.value);
|
||||
const isFullScreenRoute = computed(() => isPluginPageRoute.value);
|
||||
|
||||
const showFirstNoticeDialog = ref(false);
|
||||
|
||||
watch(isCurrentChatRoute, (isChatRoute) => {
|
||||
if (isChatRoute) {
|
||||
shouldMountChat.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const maybeShowFirstNotice = async () => {
|
||||
if (localStorage.getItem(FIRST_NOTICE_SEEN_KEY) === "1") {
|
||||
return;
|
||||
@@ -107,18 +92,11 @@ onMounted(() => {
|
||||
style="z-index: 9999; position: absolute; opacity: 0.3"
|
||||
/>
|
||||
<VerticalHeaderVue />
|
||||
<VerticalSidebarVue v-if="showSidebar" />
|
||||
<v-main
|
||||
:class="{ 'chat-main': isCurrentChatRoute }"
|
||||
:style="{
|
||||
height: isCurrentChatRoute ? '100vh' : undefined,
|
||||
overflow: isCurrentChatRoute ? 'hidden' : undefined,
|
||||
}"
|
||||
>
|
||||
<VerticalSidebarVue />
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
class="page-wrapper"
|
||||
:class="{ 'chat-mode-container': isCurrentChatRoute }"
|
||||
:style="{
|
||||
height: isFullScreenRoute ? '100%' : 'calc(100% - 8px)',
|
||||
padding: isFullScreenRoute ? '0' : undefined,
|
||||
@@ -129,18 +107,10 @@ onMounted(() => {
|
||||
:style="{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflow: isCurrentChatRoute ? 'hidden' : undefined,
|
||||
position: isPluginPageRoute ? 'relative' : undefined,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="shouldMountChat"
|
||||
v-show="isCurrentChatRoute"
|
||||
style="height: 100%; width: 100%; overflow: hidden"
|
||||
>
|
||||
<Chat :active="isCurrentChatRoute" />
|
||||
</div>
|
||||
<RouterView v-if="!isCurrentChatRoute" />
|
||||
<RouterView />
|
||||
</div>
|
||||
</v-container>
|
||||
</v-main>
|
||||
@@ -154,14 +124,3 @@ onMounted(() => {
|
||||
</v-locale-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-mode-container {
|
||||
min-height: unset !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@ import "highlight.js/styles/github.css";
|
||||
import { useI18n } from "@/i18n/composables";
|
||||
import { router } from "@/router";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDisplay, useTheme } from "vuetify";
|
||||
import { useTheme } from "vuetify";
|
||||
import StyledMenu from "@/components/shared/StyledMenu.vue";
|
||||
import { useLanguageSwitcher } from "@/i18n/composables";
|
||||
import type { Locale } from "@/i18n/types";
|
||||
@@ -29,11 +29,8 @@ const customizer = useCustomizerStore();
|
||||
const commonStore = useCommonStore();
|
||||
const chatHeader = useChatHeaderStore();
|
||||
const theme = useTheme();
|
||||
const { lgAndUp } = useDisplay();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const LAST_BOT_ROUTE_KEY = "astrbot:last_bot_route";
|
||||
const LAST_CHAT_ROUTE_KEY = "astrbot:last_chat_route";
|
||||
const SHOW_PRE_RELEASES_KEY = "astrbot:updateDialog:showPreReleases";
|
||||
let dialog = ref(false);
|
||||
let accountWarning = ref(false);
|
||||
@@ -124,21 +121,6 @@ const desktopUpdateStatus = ref("");
|
||||
const isChatPath = computed(
|
||||
() => route.path === "/chat" || route.path.startsWith("/chat/"),
|
||||
);
|
||||
const isDarkTheme = computed(
|
||||
() => theme.global.current.value.dark || customizer.uiTheme.includes("Dark"),
|
||||
);
|
||||
const chatHeaderStyle = computed(() => {
|
||||
if (!isChatPath.value) return undefined;
|
||||
const sidebarWidth = lgAndUp.value
|
||||
? customizer.chatSidebarCollapsed
|
||||
? 56
|
||||
: 280
|
||||
: 0;
|
||||
return {
|
||||
left: `${sidebarWidth}px`,
|
||||
width: `calc(100% - ${sidebarWidth}px)`,
|
||||
};
|
||||
});
|
||||
const chatHeaderSubtitleText = computed(() => {
|
||||
const title = chatHeader.title.trim();
|
||||
const subtitle = chatHeader.subtitle.trim();
|
||||
@@ -891,11 +873,7 @@ function openReleaseNotesDialog(body: string, tag: string) {
|
||||
}
|
||||
|
||||
function handleLogoClick() {
|
||||
if (isChatPath.value) {
|
||||
aboutDialog.value = true;
|
||||
} else {
|
||||
router.push("/about");
|
||||
}
|
||||
router.push("/about");
|
||||
}
|
||||
|
||||
getVersion();
|
||||
@@ -911,107 +889,12 @@ onUnmounted(() => {
|
||||
stopRestartReloadTimer();
|
||||
});
|
||||
|
||||
// 视图模式切换
|
||||
onMounted(() => {
|
||||
// 初次加載時保存當前路由
|
||||
if (typeof window !== "undefined") {
|
||||
if (isChatPath.value) {
|
||||
// 保存 chat ID
|
||||
const parts = route.fullPath.split("/");
|
||||
const sessionId = parts[2];
|
||||
if (sessionId) {
|
||||
sessionStorage.setItem(LAST_CHAT_ROUTE_KEY, sessionId);
|
||||
console.log("Initial save chat ID:", sessionId);
|
||||
}
|
||||
} else {
|
||||
// 保存 bot 路由(非 chat 頁面)
|
||||
sessionStorage.setItem(LAST_BOT_ROUTE_KEY, route.fullPath);
|
||||
console.log("Initial save bot route:", route.fullPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 viewMode 变化,切换到 bot 模式时跳转到首页
|
||||
// 保存 bot 模式的最後路由
|
||||
// 監聽 route 變化,保存最後一次 bot 路由
|
||||
watch(showPreReleases, (value) => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(SHOW_PRE_RELEASES_KEY, value ? "true" : "false");
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(newPath) => {
|
||||
if (typeof window === "undefined") return;
|
||||
console.log("Route changed:", {
|
||||
newPath,
|
||||
isChat: isChatPath.value,
|
||||
currentChatId: route.params.id,
|
||||
});
|
||||
try {
|
||||
// 使用現有的 isChatPath 計算屬性來避免名稱衝突
|
||||
const isChat = isChatPath.value; // 這裡使用已經計算好的 isChatPath
|
||||
|
||||
// ✅ bot:只存「非 chat 頁」
|
||||
if (!isChat) {
|
||||
sessionStorage.setItem(LAST_BOT_ROUTE_KEY, newPath);
|
||||
}
|
||||
|
||||
// ✅ chat:只存 sessionId
|
||||
if (isChat) {
|
||||
const parts = newPath.split("/");
|
||||
const sessionId = parts[2];
|
||||
|
||||
if (sessionId) {
|
||||
sessionStorage.setItem(LAST_CHAT_ROUTE_KEY, sessionId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to save route:", e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const currentMode = computed({
|
||||
get: () => (isChatPath.value ? "chat" : "bot"),
|
||||
set: (val: "chat" | "bot") => {
|
||||
try {
|
||||
// 檢查 window 和 sessionStorage 是否存在
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
typeof sessionStorage === "undefined"
|
||||
) {
|
||||
// 如果在非瀏覽器環境中,不做任何 sessionStorage 操作
|
||||
console.warn("sessionStorage is not available in this environment");
|
||||
return;
|
||||
}
|
||||
|
||||
if (val === "chat") {
|
||||
const lastSessionId = sessionStorage.getItem(LAST_CHAT_ROUTE_KEY);
|
||||
router.push(lastSessionId ? `/chat/${lastSessionId}` : "/chat");
|
||||
} else {
|
||||
let lastBotRoute = sessionStorage.getItem(LAST_BOT_ROUTE_KEY) || "/";
|
||||
if (lastBotRoute.startsWith("/chat")) {
|
||||
lastBotRoute = "/";
|
||||
}
|
||||
router.push(lastBotRoute);
|
||||
}
|
||||
} catch (e) {
|
||||
// 在受限隱私模式等環境中,sessionStorage 操作可能會拋出 SecurityError
|
||||
console.warn("Failed to access sessionStorage in currentMode setter:", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const mainMenuOpen = ref(false);
|
||||
const nextMode = computed<"chat" | "bot">(() =>
|
||||
isChatPath.value ? "bot" : "chat",
|
||||
);
|
||||
|
||||
function switchMode() {
|
||||
currentMode.value = nextMode.value;
|
||||
mainMenuOpen.value = false;
|
||||
}
|
||||
|
||||
// Merry Christmas! 🎄
|
||||
const isChristmas = computed(() => {
|
||||
@@ -1051,29 +934,21 @@ onMounted(async () => {
|
||||
elevation="0"
|
||||
height="50"
|
||||
class="top-header"
|
||||
:class="{
|
||||
'chat-mode-header': isChatPath,
|
||||
'chat-mode-header--dark': isChatPath && isDarkTheme,
|
||||
}"
|
||||
:absolute="isChatPath"
|
||||
:style="chatHeaderStyle"
|
||||
>
|
||||
<!-- 桌面端 menu 按钮 - 仅在 bot 模式下显示 -->
|
||||
<!-- 桌面端 menu 按钮 -->
|
||||
<v-btn
|
||||
v-if="!isChatPath"
|
||||
style="margin-left: 16px"
|
||||
class="hidden-md-and-down"
|
||||
icon
|
||||
rounded="sm"
|
||||
variant="flat"
|
||||
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)"
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER"
|
||||
>
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- 移动端 menu 按钮 -->
|
||||
<v-btn
|
||||
v-if="!isChatPath"
|
||||
class="hidden-lg-and-up ms-3"
|
||||
icon
|
||||
rounded="sm"
|
||||
@@ -1084,7 +959,6 @@ onMounted(async () => {
|
||||
</v-btn>
|
||||
|
||||
<div
|
||||
v-if="!isChatPath"
|
||||
class="logo-container"
|
||||
:class="{
|
||||
'mobile-logo': $vuetify.display.xs,
|
||||
@@ -1118,6 +992,9 @@ onMounted(async () => {
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<!-- 版本提示信息 - 在手机上隐藏 -->
|
||||
<div
|
||||
v-if="isChatPath"
|
||||
class="chat-header-context"
|
||||
@@ -1128,10 +1005,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<!-- 版本提示信息 - 在手机上隐藏 -->
|
||||
<div v-if="!isChatPath" class="mr-4 hidden-xs">
|
||||
<div v-else class="mr-4 hidden-xs">
|
||||
<small v-if="hasNewVersion">
|
||||
{{ t("core.header.version.hasNewVersion") }}
|
||||
</small>
|
||||
@@ -1140,33 +1014,16 @@ onMounted(async () => {
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="header-actions" :class="{ 'chat-header-actions': isChatPath }">
|
||||
<!-- Bot/Chat mode switch - single button, hidden in chat mobile menu -->
|
||||
<v-btn
|
||||
v-if="!isChatPath || !$vuetify.display.smAndDown"
|
||||
class="mode-switch-btn"
|
||||
:class="{ 'mr-4 hidden-xs': !isChatPath }"
|
||||
variant="text"
|
||||
size="small"
|
||||
rounded="sm"
|
||||
@click="switchMode"
|
||||
>
|
||||
<v-icon start>{{ nextMode === "bot" ? "mdi-robot" : "mdi-chat" }}</v-icon>
|
||||
{{ nextMode === "bot" ? "Bot" : "Chat" }}
|
||||
</v-btn>
|
||||
|
||||
<div class="header-actions">
|
||||
<!-- 功能菜单 -->
|
||||
<StyledMenu v-model="mainMenuOpen" offset="12" location="bottom end">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
v-bind="activatorProps"
|
||||
size="small"
|
||||
:class="[
|
||||
'action-btn',
|
||||
isChatPath ? 'chat-action-btn' : 'mr-4',
|
||||
]"
|
||||
:color="isChatPath ? undefined : 'var(--v-theme-surface)'"
|
||||
:variant="isChatPath ? 'text' : 'flat'"
|
||||
class="action-btn mr-4"
|
||||
color="var(--v-theme-surface)"
|
||||
variant="flat"
|
||||
rounded="sm"
|
||||
icon
|
||||
>
|
||||
@@ -1174,29 +1031,6 @@ onMounted(async () => {
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- Bot/Chat 模式切换 - 仅在手机端显示 -->
|
||||
<template
|
||||
v-if="
|
||||
(isChatPath && $vuetify.display.smAndDown) ||
|
||||
(!isChatPath && $vuetify.display.xs)
|
||||
"
|
||||
>
|
||||
<div class="mobile-mode-switch-wrapper">
|
||||
<v-btn
|
||||
class="mobile-mode-switch-btn"
|
||||
variant="text"
|
||||
block
|
||||
@click="switchMode"
|
||||
>
|
||||
<v-icon start>{{
|
||||
nextMode === "bot" ? "mdi-robot" : "mdi-chat"
|
||||
}}</v-icon>
|
||||
{{ nextMode === "bot" ? "Bot" : "Chat" }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-divider class="my-1" />
|
||||
</template>
|
||||
|
||||
<!-- 语言切换分组 -->
|
||||
<v-menu
|
||||
open-on-click
|
||||
@@ -2033,21 +1867,6 @@ onMounted(async () => {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.top-header.chat-mode-header {
|
||||
background: #fdfcfc !important;
|
||||
border-bottom: 0;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.top-header.chat-mode-header.chat-mode-header--dark {
|
||||
background: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
|
||||
.top-header.chat-mode-header .v-toolbar__content {
|
||||
padding: 0 16px 0 20px;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.chat-mobile-sidebar-toggle {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
@@ -2071,13 +1890,15 @@ onMounted(async () => {
|
||||
|
||||
.chat-header-context {
|
||||
min-width: 0;
|
||||
max-width: clamp(180px, calc(100vw - 600px), 460px);
|
||||
max-width: min(360px, 38vw);
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 5px 18px 5px 0;
|
||||
padding: 5px 10px 5px 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chat-header-context .provider-trigger--header {
|
||||
@@ -2090,7 +1911,7 @@ onMounted(async () => {
|
||||
|
||||
.chat-header-context .provider-trigger-title {
|
||||
min-width: 0;
|
||||
max-width: min(300px, 52vw);
|
||||
max-width: min(240px, 30vw);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -2117,41 +1938,6 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-header-actions {
|
||||
gap: 4px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mode-switch-btn {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mode-switch-btn {
|
||||
min-width: 62px;
|
||||
background: transparent !important;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.mode-switch-btn .v-btn__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.mode-switch-btn .v-icon {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.chat-action-btn {
|
||||
margin-right: 0;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
/* 响应式布局样式 */
|
||||
.logo-container {
|
||||
margin-left: 10px;
|
||||
@@ -2240,30 +2026,6 @@ onMounted(async () => {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.mobile-mode-switch-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 12px 4px;
|
||||
}
|
||||
|
||||
.mobile-mode-switch-btn {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.mobile-mode-switch-btn .v-icon {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.mobile-mode-switch-btn .v-btn__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
/* 移动端对话框标题样式 */
|
||||
.mobile-card-title {
|
||||
display: flex;
|
||||
@@ -2472,17 +2234,8 @@ onMounted(async () => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-header-actions .chat-action-btn,
|
||||
.chat-header-actions .mode-switch-btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.top-header.chat-mode-header .v-toolbar__content {
|
||||
padding: 0 12px 0 14px;
|
||||
}
|
||||
|
||||
.chat-header-context {
|
||||
max-width: calc(100vw - 92px);
|
||||
max-width: calc(100vw - 210px);
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup>
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({ item: Object, level: Number });
|
||||
const props = defineProps({ item: Object, level: Number, rail: Boolean });
|
||||
const { t } = useI18n();
|
||||
const customizer = useCustomizerStore();
|
||||
const route = useRoute();
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const lvl = props.level ?? 0;
|
||||
const indent = customizer.mini_sidebar ? '0px' : `${lvl * 24}px`;
|
||||
const indent = props.rail ? '0px' : `${lvl * 24}px`;
|
||||
return { '--indent-padding': indent };
|
||||
});
|
||||
|
||||
@@ -22,7 +20,11 @@ const isItemActive = computed(() => {
|
||||
const [path, hash] = props.item.to.split('#');
|
||||
return route.path === path && route.hash === `#${hash}`;
|
||||
}
|
||||
return route.path === props.item.to;
|
||||
const targetPath = props.item.to.replace(/\/$/, '') || '/';
|
||||
if (targetPath === '/') {
|
||||
return route.path === targetPath;
|
||||
}
|
||||
return route.path === targetPath || route.path.startsWith(`${targetPath}/`);
|
||||
});
|
||||
|
||||
const itemTitle = computed(() => {
|
||||
@@ -33,9 +35,19 @@ const itemTitle = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-group v-if="item.children" :value="item.title" :class="{ 'group-bordered': customizer.mini_sidebar }">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
|
||||
<v-list-group v-if="item.children" :value="item.title" :class="{ 'rail-group': rail }">
|
||||
<template v-slot:activator="{ props: groupProps }">
|
||||
<v-tooltip v-if="rail" location="right" :text="itemTitle" open-delay="180">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-list-item v-bind="{ ...groupProps, ...tooltipProps }" rounded class="mb-1" color="secondary"
|
||||
:prepend-icon="item.icon" :style="{ '--indent-padding': '0px' }" :aria-label="itemTitle">
|
||||
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
|
||||
{{ itemTitle }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-list-item v-else v-bind="groupProps" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
|
||||
:style="{ '--indent-padding': '0px' }">
|
||||
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
|
||||
{{ itemTitle }}
|
||||
@@ -45,10 +57,33 @@ const itemTitle = computed(() => {
|
||||
|
||||
<!-- children -->
|
||||
<template v-for="(child, index) in item.children" :key="child.title || child.to || `child-${index}`">
|
||||
<NavItem :item="child" :level="(level || 0) + 1" />
|
||||
<NavItem :item="child" :level="(level || 0) + 1" :rail="rail" />
|
||||
</template>
|
||||
</v-list-group>
|
||||
|
||||
<v-tooltip v-else-if="rail" location="right" :text="itemTitle" open-delay="180">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-list-item v-bind="tooltipProps" :to="item.type === 'external' ? '' : item.to"
|
||||
:href="item.type === 'external' ? item.to : ''" :active="isItemActive" rounded class="mb-1"
|
||||
color="secondary" :disabled="item.disabled" :target="item.type === 'external' ? '_blank' : ''"
|
||||
:style="itemStyle" :aria-label="itemTitle">
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title style="font-size: 14px;">{{ itemTitle }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
|
||||
{{ item.subCaption }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append v-if="item.chip">
|
||||
<v-chip :color="item.chipColor" class="sidebarchip hide-menu" :size="item.chipIcon ? 'small' : 'default'"
|
||||
:variant="item.chipVariant" :prepend-icon="item.chipIcon">
|
||||
{{ item.chip }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''"
|
||||
:active="isItemActive" rounded class="mb-1" color="secondary" :disabled="item.disabled"
|
||||
:target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
|
||||
@@ -69,10 +104,16 @@ const itemTitle = computed(() => {
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 在折叠(mini)状态下,分组展开时给整个分组(母项+子项)加边框以便区分 */
|
||||
.group-bordered.v-list-group--open {
|
||||
border: 2px solid rgba(var(--v-theme-borderLight), 0.35);
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--v-theme-borderLight), 0.04);
|
||||
.rail-group {
|
||||
border-radius: 12px;
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.rail-group.v-list-group--open {
|
||||
background: rgba(var(--v-theme-primary), 0.06);
|
||||
}
|
||||
|
||||
.rail-group.v-list-group--open > .v-list-group__items {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<script setup>
|
||||
import { ref, shallowRef, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { useCustomizerStore } from '../../../stores/customizer';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import sidebarItems, { MORE_GROUP_KEY } from './sidebarItem';
|
||||
import NavItem from './NavItem.vue';
|
||||
import { applySidebarCustomization } from '@/utils/sidebarCustomization';
|
||||
import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
|
||||
import { usePluginSidebarItems } from '@/composables/usePluginSidebarItems';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { t } = useI18n();
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const theme = useTheme();
|
||||
const { pluginItems } = usePluginSidebarItems();
|
||||
|
||||
function buildSidebarMenu() {
|
||||
@@ -105,159 +102,23 @@ onUnmounted(() => {
|
||||
window.removeEventListener('sidebar-customization-changed', handleCustomEvent);
|
||||
});
|
||||
|
||||
const showIframe = ref(false);
|
||||
const starCount = ref(null);
|
||||
|
||||
// 更新日志对话框
|
||||
const changelogDialog = ref(false);
|
||||
|
||||
const sidebarWidth = ref(235);
|
||||
const minSidebarWidth = 200;
|
||||
const maxSidebarWidth = 300;
|
||||
const isResizing = ref(false);
|
||||
|
||||
const isDark = computed(() => customizer.uiTheme === 'PurpleThemeDark');
|
||||
const themeColors = computed(() => theme.current.value.colors);
|
||||
const iframeBackground = computed(() => isDark.value ? themeColors.value.surface || 'white' : 'white');
|
||||
const dragHeaderBackground = computed(() => isDark.value ? themeColors.value.mcpCardBg || themeColors.value.surface || 'white' : '#f0f0f0');
|
||||
const frameBorder = computed(() => `1px solid ${isDark.value ? (themeColors.value.borderLight || '#ccc') : '#ccc'}`);
|
||||
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const isRailSidebar = !isMobile;
|
||||
if (isMobile) {
|
||||
customizer.Sidebar_drawer = false;
|
||||
} else {
|
||||
customizer.SET_MINI_SIDEBAR(true);
|
||||
}
|
||||
|
||||
const dragPos = ref({ left: '', top: '' });
|
||||
|
||||
const iframeStyle = computed(() => {
|
||||
const base = isMobile
|
||||
? { position: 'fixed', top: '10%', left: '0%', width: '100%', height: '80%', zIndex: '1002' }
|
||||
: { position: 'fixed', bottom: '16px', right: '16px', width: '490px', height: '640px', zIndex: '10000000' };
|
||||
const pos = dragPos.value.left ? { left: dragPos.value.left, top: dragPos.value.top, bottom: 'auto', right: 'auto' } : {};
|
||||
return {
|
||||
...base,
|
||||
...pos,
|
||||
minWidth: '300px',
|
||||
minHeight: '200px',
|
||||
background: iframeBackground.value,
|
||||
resize: 'both',
|
||||
overflow: 'auto',
|
||||
borderRadius: '12px',
|
||||
boxShadow: isDark.value ? '0px 4px 16px rgba(0, 0, 0, 0.5)' : '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
};
|
||||
});
|
||||
|
||||
const iframeInnerStyle = computed(() => ({
|
||||
width: '100%',
|
||||
height: 'calc(100% - 66px)',
|
||||
border: 'none',
|
||||
borderBottomLeftRadius: '12px',
|
||||
borderBottomRightRadius: '12px',
|
||||
filter: isDark.value ? 'invert(0.88) hue-rotate(180deg)' : 'none',
|
||||
}));
|
||||
|
||||
const dragHeaderStyle = computed(() => ({
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
background: dragHeaderBackground.value,
|
||||
borderBottom: frameBorder.value,
|
||||
borderTopLeftRadius: '8px',
|
||||
borderTopRightRadius: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'move'
|
||||
}));
|
||||
|
||||
function toggleIframe() {
|
||||
showIframe.value = !showIframe.value;
|
||||
}
|
||||
|
||||
function openIframeLink(url) {
|
||||
if (typeof window !== 'undefined') {
|
||||
let url_ = url || "https://docs.astrbot.app";
|
||||
window.open(url_, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
function openFaqLink() {
|
||||
const faqUrl = locale.value === 'en-US'
|
||||
? 'https://docs.astrbot.app/en/faq.html'
|
||||
: 'https://docs.astrbot.app/faq.html';
|
||||
openIframeLink(faqUrl);
|
||||
}
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let isDragging = false;
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function startDrag(clientX, clientY) {
|
||||
isDragging = true;
|
||||
const dm = document.getElementById('draggable-iframe');
|
||||
const rect = dm.getBoundingClientRect();
|
||||
offsetX = clientX - rect.left;
|
||||
offsetY = clientY - rect.top;
|
||||
document.body.style.userSelect = 'none';
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
|
||||
function onMouseDown(event) {
|
||||
startDrag(event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
if (isDragging) {
|
||||
moveAt(event.clientX, event.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
endDrag();
|
||||
}
|
||||
|
||||
function onTouchStart(event) {
|
||||
if (event.touches.length === 1) {
|
||||
const touch = event.touches[0];
|
||||
startDrag(touch.clientX, touch.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
function onTouchMove(event) {
|
||||
if (isDragging && event.touches.length === 1) {
|
||||
event.preventDefault();
|
||||
const touch = event.touches[0];
|
||||
moveAt(touch.clientX, touch.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
endDrag();
|
||||
}
|
||||
|
||||
function moveAt(clientX, clientY) {
|
||||
const dm = document.getElementById('draggable-iframe');
|
||||
const newLeft = clamp(clientX - offsetX, 0, window.innerWidth - dm.offsetWidth);
|
||||
const newTop = clamp(clientY - offsetY, 0, window.innerHeight - dm.offsetHeight);
|
||||
// Sync dragged position to reactive variable
|
||||
dragPos.value = { left: newLeft + 'px', top: newTop + 'px' };
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
isDragging = false;
|
||||
document.body.style.userSelect = '';
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
|
||||
function startSidebarResize(event) {
|
||||
isResizing.value = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
@@ -291,30 +152,6 @@ function startSidebarResize(event) {
|
||||
document.addEventListener('mouseup', onMouseUpResize);
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
async function fetchStarCount() {
|
||||
try {
|
||||
const response = await fetch('https://cloud.astrbot.app/api/v1/github/repo-info');
|
||||
const data = await response.json();
|
||||
if (data.data && data.data.stargazers_count) {
|
||||
starCount.value = data.data.stargazers_count;
|
||||
console.debug('Fetched star count:', starCount.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('Failed to fetch star count:', error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchStarCount();
|
||||
|
||||
// 打开更新日志对话框
|
||||
function openChangelogDialog() {
|
||||
changelogDialog.value = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -326,46 +163,27 @@ function openChangelogDialog() {
|
||||
app
|
||||
class="leftSidebar"
|
||||
:width="sidebarWidth"
|
||||
:rail="customizer.mini_sidebar"
|
||||
:rail="isRailSidebar"
|
||||
>
|
||||
<div class="sidebar-container">
|
||||
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': customizer.mini_sidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
|
||||
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': isRailSidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="item.title || item.to || `sidebar-item-${i}`">
|
||||
<NavItem :item="item" class="leftPadding" />
|
||||
<NavItem :item="item" class="leftPadding" :rail="isRailSidebar" />
|
||||
</template>
|
||||
</v-list>
|
||||
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="tonal" color="primary" to="/settings" prepend-icon="mdi-cog">
|
||||
{{ t('core.navigation.settings') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-note-text-outline"
|
||||
@click="openChangelogDialog">
|
||||
{{ t('core.navigation.changelog') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-book-open-variant"
|
||||
@click="toggleIframe">
|
||||
{{ t('core.navigation.documentation') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-frequently-asked-questions"
|
||||
@click="openFaqLink">
|
||||
{{ t('core.navigation.faq') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-github"
|
||||
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
{{ t('core.navigation.github') }}
|
||||
<v-chip
|
||||
v-if="starCount"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="ml-2"
|
||||
style="font-weight: normal;"
|
||||
>{{ formatNumber(starCount) }}</v-chip>
|
||||
<div class="sidebar-footer">
|
||||
<v-btn class="sidebar-footer-btn" :class="{ 'sidebar-footer-icon-btn': isRailSidebar }" :size="isRailSidebar ? 'default' : 'small'"
|
||||
:variant="isRailSidebar ? 'text' : 'tonal'" :color="isRailSidebar ? undefined : 'primary'" to="/settings"
|
||||
:prepend-icon="isRailSidebar ? undefined : 'mdi-cog'" :aria-label="t('core.navigation.settings')">
|
||||
<v-icon v-if="isRailSidebar" icon="mdi-cog" />
|
||||
<template v-else>{{ t('core.navigation.settings') }}</template>
|
||||
<v-tooltip v-if="isRailSidebar" activator="parent" location="right" :text="t('core.navigation.settings')" open-delay="180" />
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!customizer.mini_sidebar && customizer.Sidebar_drawer"
|
||||
v-if="!isRailSidebar && !isMobile && customizer.Sidebar_drawer"
|
||||
class="sidebar-resize-handle"
|
||||
@mousedown="startSidebarResize"
|
||||
:class="{ 'resizing': isResizing }"
|
||||
@@ -373,44 +191,6 @@ function openChangelogDialog() {
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<div
|
||||
v-if="showIframe"
|
||||
id="draggable-iframe"
|
||||
:style="iframeStyle"
|
||||
>
|
||||
|
||||
<div :style="dragHeaderStyle" @mousedown="onMouseDown" @touchstart="onTouchStart">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<v-icon icon="mdi-cursor-move" />
|
||||
<span style="margin-left: 8px;">{{ t('core.navigation.drag') }}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<v-btn
|
||||
icon
|
||||
@click.stop="openIframeLink('https://docs.astrbot.app')"
|
||||
@mousedown.stop
|
||||
:style="{ borderRadius: '8px', border: frameBorder }"
|
||||
>
|
||||
<v-icon icon="mdi-open-in-new" />
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
@click.stop="toggleIframe"
|
||||
@mousedown.stop
|
||||
:style="{ borderRadius: '8px', border: frameBorder }"
|
||||
>
|
||||
<v-icon icon="mdi-close" />
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
src="https://docs.astrbot.app"
|
||||
:style="iframeInnerStyle"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<!-- 更新日志对话框 -->
|
||||
<ChangelogDialog v-model="changelogDialog" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -26,6 +26,11 @@ const sidebarItem: menu[] = [
|
||||
icon: 'mdi-hand-wave-outline',
|
||||
to: '/welcome',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.chat',
|
||||
icon: 'mdi-star-outline',
|
||||
to: '/chat',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.platforms',
|
||||
icon: 'mdi-robot',
|
||||
@@ -44,17 +49,12 @@ const sidebarItem: menu[] = [
|
||||
{
|
||||
title: 'core.navigation.extension',
|
||||
icon: 'mdi-puzzle',
|
||||
to: '/extension#installed',
|
||||
to: '/extension',
|
||||
children: [
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.installed',
|
||||
icon: 'mdi-puzzle',
|
||||
to: '/extension#installed'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.market',
|
||||
icon: 'mdi-store',
|
||||
to: '/extension#market'
|
||||
to: '/extension'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extensionTabs.mcp',
|
||||
|
||||
@@ -4,16 +4,14 @@ const ChatBoxRoutes = {
|
||||
children: [
|
||||
{
|
||||
name: 'ChatBox',
|
||||
path: '/chatbox',
|
||||
path: '',
|
||||
component: () => import('@/views/ChatBoxPage.vue')
|
||||
},
|
||||
{
|
||||
path: ':conversationId',
|
||||
name: 'ChatBoxDetail',
|
||||
component: () => import('@/views/ChatBoxPage.vue'),
|
||||
children: [
|
||||
{
|
||||
path: ':conversationId',
|
||||
name: 'ChatBoxDetail',
|
||||
component: () => import('@/views/ChatBoxPage.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
props: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -159,16 +159,9 @@ const MainRoutes = {
|
||||
// },
|
||||
{
|
||||
name: 'Chat',
|
||||
path: '/chat',
|
||||
path: '/chat/:conversationId?',
|
||||
component: () => import('@/views/ChatPage.vue'),
|
||||
children: [
|
||||
{
|
||||
path: ':conversationId',
|
||||
name: 'ChatDetail',
|
||||
component: () => import('@/views/ChatPage.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
props: true
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
|
||||
@@ -65,6 +65,101 @@
|
||||
.leftPadding {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.listitem {
|
||||
padding: 10px !important;
|
||||
|
||||
.v-list-group__items .v-list-item,
|
||||
.v-list-item {
|
||||
width: 56px;
|
||||
min-width: 56px;
|
||||
max-width: 56px;
|
||||
min-height: 50px;
|
||||
margin-bottom: 8px !important;
|
||||
margin-inline: auto;
|
||||
padding-inline: 0 !important;
|
||||
grid-template-areas: "prepend" !important;
|
||||
grid-template-columns: 1fr !important;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.rail-group {
|
||||
width: 56px;
|
||||
margin-bottom: 8px;
|
||||
margin-inline: auto;
|
||||
padding: 4px 0 2px;
|
||||
|
||||
.v-list-item {
|
||||
width: 56px;
|
||||
min-width: 56px;
|
||||
max-width: 56px;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item__prepend {
|
||||
grid-area: prepend;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0 !important;
|
||||
justify-content: center;
|
||||
margin-inline-end: 0;
|
||||
|
||||
.v-icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item__spacer,
|
||||
.v-list-item__append,
|
||||
.v-list-item__content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
width: 100%;
|
||||
padding: 12px 10px 18px !important;
|
||||
gap: 8px;
|
||||
|
||||
.sidebar-footer-icon-btn {
|
||||
width: 56px !important;
|
||||
min-width: 56px !important;
|
||||
max-width: 56px !important;
|
||||
height: 52px !important;
|
||||
border-radius: 14px !important;
|
||||
color: rgba(var(--v-theme-on-surface), 0.82);
|
||||
margin-bottom: 0 !important;
|
||||
padding: 0 !important;
|
||||
justify-content: center !important;
|
||||
text-align: center;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
|
||||
.v-btn__content {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
font-size: 22px;
|
||||
margin: 0 !important;
|
||||
opacity: 0.98;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.v-btn--active {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
background: rgba(var(--v-theme-primary), 0.09);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1170px) {
|
||||
.mini-sidebar {
|
||||
@@ -122,3 +217,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-navigation-drawer--rail.leftSidebar {
|
||||
.sidebar-container .sidebar-footer {
|
||||
.sidebar-footer-btn.sidebar-footer-icon-btn {
|
||||
justify-content: center !important;
|
||||
text-align: center;
|
||||
|
||||
.v-btn__content {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import Chat from '@/components/chat/Chat.vue'
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
height: calc(100vh - 60px)
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 66px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -390,7 +390,11 @@ const updateDialogPluginLogo = computed(() => {
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="activeTab === 'market'" cols="12" md="12">
|
||||
<v-col
|
||||
v-if="activeTab === 'installed' || activeTab === 'market'"
|
||||
cols="12"
|
||||
md="12"
|
||||
>
|
||||
<div class="d-flex align-center justify-center mt-4 mb-4 gap-4">
|
||||
<v-btn
|
||||
variant="text"
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="settings-nav__item"
|
||||
:class="{ 'settings-nav__item--active': activeSettingsSection === item.id }"
|
||||
:class="{
|
||||
'settings-nav__item--active': activeSettingsSection === item.id,
|
||||
'settings-nav__item--divider': item.dividerBefore
|
||||
}"
|
||||
:aria-pressed="activeSettingsSection === item.id"
|
||||
@click="activeSettingsSection = item.id"
|
||||
>
|
||||
@@ -147,6 +150,33 @@
|
||||
<div class="settings-section__title">{{ tm('sections.network.title') }}</div>
|
||||
</div>
|
||||
<div class="settings-section__content">
|
||||
<div class="settings-list-card">
|
||||
<div class="settings-item">
|
||||
<div class="settings-item__label">
|
||||
<div class="settings-item__title">{{ tm('network.transport.title') }}</div>
|
||||
<div class="settings-item__subtitle">{{ tm('network.transport.subtitle') }}</div>
|
||||
</div>
|
||||
<div class="settings-item__control settings-item__control--transport">
|
||||
<v-btn-toggle
|
||||
v-model="chatTransportMode"
|
||||
mandatory
|
||||
divided
|
||||
density="compact"
|
||||
color="primary"
|
||||
class="transport-mode-toggle"
|
||||
>
|
||||
<v-btn
|
||||
v-for="option in chatTransportOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.title }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!systemConfigLoading">
|
||||
<div
|
||||
v-for="group in networkSystemConfigGroups"
|
||||
@@ -395,12 +425,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="settings-resources" class="settings-section" v-show="activeSettingsSection === 'resources'">
|
||||
<div class="settings-section__heading">
|
||||
<div class="settings-section__title">{{ tm('sections.resources.title') }}</div>
|
||||
</div>
|
||||
<div class="settings-section__content">
|
||||
<div class="settings-list-card">
|
||||
<div
|
||||
v-for="item in resourceItems"
|
||||
:key="item.key"
|
||||
class="settings-item"
|
||||
>
|
||||
<div class="settings-item__label">
|
||||
<div class="settings-item__title">{{ item.title }}</div>
|
||||
<div class="settings-item__subtitle">{{ item.subtitle }}</div>
|
||||
</div>
|
||||
<div class="settings-item__control">
|
||||
<v-btn variant="tonal" @click="item.action()">
|
||||
<v-icon class="mr-2">{{ item.icon }}</v-icon>
|
||||
{{ item.title }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WaitingForRestart ref="wfr" />
|
||||
<BackupDialog ref="backupDialog" />
|
||||
<ChangelogDialog v-model="changelogDialog" />
|
||||
<DashboardTwoFactorDialog
|
||||
v-model="configSave2faDialogVisible"
|
||||
:error-message="configSave2faError"
|
||||
@@ -421,9 +478,10 @@ import SidebarCustomizer from '@/components/shared/SidebarCustomizer.vue';
|
||||
import BackupDialog from '@/components/shared/BackupDialog.vue';
|
||||
import StorageCleanupPanel from '@/components/shared/StorageCleanupPanel.vue';
|
||||
import DashboardTwoFactorDialog from '@/components/shared/DashboardTwoFactorDialog.vue';
|
||||
import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
|
||||
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { PurpleTheme } from '@/theme/LightTheme';
|
||||
import { useToastStore } from '@/stores/toast';
|
||||
@@ -431,6 +489,7 @@ import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog';
|
||||
|
||||
const { tm } = useModuleI18n('features/settings');
|
||||
const { tm: tmMeta } = useModuleI18n('features/config-metadata');
|
||||
const { t, locale } = useI18n();
|
||||
const toastStore = useToastStore();
|
||||
const confirmDialog = useConfirmDialog();
|
||||
const theme = useTheme();
|
||||
@@ -442,6 +501,11 @@ const getStoredColor = (key, fallback) => {
|
||||
|
||||
const primaryColor = ref(getStoredColor('themePrimary', PurpleTheme.colors.primary));
|
||||
const secondaryColor = ref(getStoredColor('themeSecondary', PurpleTheme.colors.secondary));
|
||||
const chatTransportMode = ref(
|
||||
typeof window !== 'undefined' && localStorage.getItem('chat.transportMode') === 'websocket'
|
||||
? 'websocket'
|
||||
: 'sse'
|
||||
);
|
||||
|
||||
const resolveThemes = () => {
|
||||
if (theme?.themes?.value) return theme.themes.value;
|
||||
@@ -476,6 +540,11 @@ watch(secondaryColor, (value) => {
|
||||
applyThemeColors(primaryColor.value, value);
|
||||
});
|
||||
|
||||
watch(chatTransportMode, (value) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem('chat.transportMode', value === 'websocket' ? 'websocket' : 'sse');
|
||||
});
|
||||
|
||||
const wfr = ref(null);
|
||||
const backupDialog = ref(null);
|
||||
const apiKeys = ref([]);
|
||||
@@ -497,6 +566,7 @@ const configSave2faRotationHint = ref('');
|
||||
const configSavePendingData = ref(null);
|
||||
const systemConfigAutoSaveTimer = ref(null);
|
||||
const activeSettingsSection = ref('general');
|
||||
const changelogDialog = ref(false);
|
||||
|
||||
const apiKeyExpiryOptions = computed(() => [
|
||||
{ title: tm('apiKey.expiryOptions.day1'), value: 1 },
|
||||
@@ -520,13 +590,61 @@ const availableScopes = [
|
||||
{ value: 'skill', label: 'skill' }
|
||||
];
|
||||
|
||||
const chatTransportOptions = computed(() => [
|
||||
{ title: tm('network.transport.sse'), value: 'sse' },
|
||||
{ title: tm('network.transport.websocket'), value: 'websocket' }
|
||||
]);
|
||||
|
||||
const settingsNavItems = computed(() => [
|
||||
{ id: 'general', label: tm('sections.general.title'), icon: 'mdi mdi-tune-variant' },
|
||||
{ id: 'appearance', label: tm('sections.appearance.title'), icon: 'mdi mdi-palette-outline' },
|
||||
{ id: 'network', label: tm('sections.network.title'), icon: 'mdi mdi-lan-connect' },
|
||||
{ id: 'security', label: tm('sections.security.title'), icon: 'mdi mdi-shield-lock-outline' },
|
||||
{ id: 'maintenance', label: tm('sections.maintenance.title'), icon: 'mdi mdi-tools' },
|
||||
{ id: 'openapi', label: tm('sections.openapi.title'), icon: 'mdi mdi-api' }
|
||||
{ id: 'openapi', label: tm('sections.openapi.title'), icon: 'mdi mdi-api' },
|
||||
{ id: 'resources', label: tm('sections.resources.title'), icon: 'mdi mdi-information-outline', dividerBefore: true }
|
||||
]);
|
||||
|
||||
const openExternalLink = (url) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const openFaqLink = () => {
|
||||
openExternalLink(locale.value === 'en-US'
|
||||
? 'https://docs.astrbot.app/en/faq.html'
|
||||
: 'https://docs.astrbot.app/faq.html');
|
||||
};
|
||||
|
||||
const resourceItems = computed(() => [
|
||||
{
|
||||
key: 'changelog',
|
||||
title: t('core.navigation.changelog'),
|
||||
subtitle: tm('resources.changelog.subtitle'),
|
||||
icon: 'mdi-note-text-outline',
|
||||
action: () => { changelogDialog.value = true; }
|
||||
},
|
||||
{
|
||||
key: 'documentation',
|
||||
title: t('core.navigation.documentation'),
|
||||
subtitle: tm('resources.documentation.subtitle'),
|
||||
icon: 'mdi-book-open-variant',
|
||||
action: () => openExternalLink('https://docs.astrbot.app')
|
||||
},
|
||||
{
|
||||
key: 'faq',
|
||||
title: t('core.navigation.faq'),
|
||||
subtitle: tm('resources.faq.subtitle'),
|
||||
icon: 'mdi-frequently-asked-questions',
|
||||
action: openFaqLink
|
||||
},
|
||||
{
|
||||
key: 'github',
|
||||
title: t('core.navigation.github'),
|
||||
subtitle: tm('resources.github.subtitle'),
|
||||
icon: 'mdi-github',
|
||||
action: () => openExternalLink('https://github.com/AstrBotDevs/AstrBot')
|
||||
}
|
||||
]);
|
||||
|
||||
const configIncludedScopes = ['bot', 'provider'];
|
||||
@@ -916,6 +1034,8 @@ onMounted(async () => {
|
||||
activeSettingsSection.value = 'maintenance';
|
||||
} else if (hash.includes('settings-openapi')) {
|
||||
activeSettingsSection.value = 'openapi';
|
||||
} else if (hash.includes('settings-resources')) {
|
||||
activeSettingsSection.value = 'resources';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -995,6 +1115,20 @@ onUnmounted(() => {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.settings-nav__item--divider {
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.settings-nav__item--divider::after {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background: var(--settings-divider);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.settings-nav__item--active {
|
||||
background: rgba(var(--v-theme-on-surface), 0.07);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
@@ -1132,6 +1266,23 @@ onUnmounted(() => {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.settings-item__control--transport {
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.transport-mode-toggle {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--settings-border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.transport-mode-toggle :deep(.v-btn) {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.settings-item__control :deep(.v-btn) {
|
||||
min-height: 36px;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -221,7 +221,7 @@ const togglePinnedExtension = (extension) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-tab-item v-show="activeTab === 'installed'">
|
||||
<v-tab-item v-show="activeTab === 'installed' || activeTab === 'market'">
|
||||
<div class="mb-4 pt-4 pb-4">
|
||||
<div class="d-flex align-center flex-wrap" style="gap: 12px">
|
||||
<h2 class="text-h2 mb-0">{{ tm("titles.installedAstrBotPlugins") }}</h2>
|
||||
|
||||
@@ -184,7 +184,7 @@ const openMarketPluginDetail = (plugin) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-tab-item v-show="activeTab === 'market'">
|
||||
<v-tab-item v-show="activeTab === 'installed' || activeTab === 'market'">
|
||||
<div class="mb-6 pt-4 pb-4">
|
||||
<div class="d-flex align-center" style="gap: 12px">
|
||||
<div class="d-flex align-center" style="gap: 12px; min-width: 0">
|
||||
|
||||
@@ -1360,105 +1360,6 @@ async def test_generated_password_requires_password_change_until_changed(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("endpoint", "method"),
|
||||
[
|
||||
("/api/auth/account/edit", "post"),
|
||||
("/api/v1/auth/account", "patch"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("new_username", ["ab", " "])
|
||||
async def test_account_edit_rejects_invalid_username(
|
||||
app: FastAPIAppAdapter,
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
endpoint: str,
|
||||
method: str,
|
||||
new_username: str,
|
||||
):
|
||||
original_dashboard_config = copy.deepcopy(
|
||||
core_lifecycle_td.astrbot_config["dashboard"]
|
||||
)
|
||||
test_client = app.test_client()
|
||||
current_username = core_lifecycle_td.astrbot_config["dashboard"]["username"]
|
||||
current_password = _resolve_dashboard_password(core_lifecycle_td)
|
||||
|
||||
try:
|
||||
login_response = await test_client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": current_username, "password": current_password},
|
||||
)
|
||||
login_data = await login_response.get_json()
|
||||
assert login_data["status"] == "ok"
|
||||
headers = {"Authorization": f"Bearer {login_data['data']['token']}"}
|
||||
|
||||
payload = {
|
||||
"password": current_password,
|
||||
"new_password": "",
|
||||
"confirm_password": "",
|
||||
"new_username": new_username,
|
||||
}
|
||||
request = getattr(test_client, method)
|
||||
response = await request(endpoint, headers=headers, json=payload)
|
||||
data = await response.get_json()
|
||||
|
||||
assert data["status"] == "error"
|
||||
assert data["message"] == "用户名长度至少3位"
|
||||
assert (
|
||||
core_lifecycle_td.astrbot_config["dashboard"]["username"]
|
||||
== (original_dashboard_config["username"])
|
||||
)
|
||||
finally:
|
||||
await _restore_dashboard_password_state(
|
||||
core_lifecycle_td,
|
||||
original_dashboard_config,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_account_edit_trims_valid_username(
|
||||
app: FastAPIAppAdapter,
|
||||
core_lifecycle_td: AstrBotCoreLifecycle,
|
||||
):
|
||||
original_dashboard_config = copy.deepcopy(
|
||||
core_lifecycle_td.astrbot_config["dashboard"]
|
||||
)
|
||||
test_client = app.test_client()
|
||||
current_username = core_lifecycle_td.astrbot_config["dashboard"]["username"]
|
||||
current_password = _resolve_dashboard_password(core_lifecycle_td)
|
||||
|
||||
try:
|
||||
login_response = await test_client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": current_username, "password": current_password},
|
||||
)
|
||||
login_data = await login_response.get_json()
|
||||
assert login_data["status"] == "ok"
|
||||
headers = {"Authorization": f"Bearer {login_data['data']['token']}"}
|
||||
|
||||
response = await test_client.post(
|
||||
"/api/auth/account/edit",
|
||||
headers=headers,
|
||||
json={
|
||||
"password": current_password,
|
||||
"new_password": "",
|
||||
"confirm_password": "",
|
||||
"new_username": " astrbot-admin ",
|
||||
},
|
||||
)
|
||||
data = await response.get_json()
|
||||
|
||||
assert data["status"] == "ok"
|
||||
assert core_lifecycle_td.astrbot_config["dashboard"]["username"] == (
|
||||
"astrbot-admin"
|
||||
)
|
||||
finally:
|
||||
await _restore_dashboard_password_state(
|
||||
core_lifecycle_td,
|
||||
original_dashboard_config,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_setup_can_skip_default_password_auth(
|
||||
app: FastAPIAppAdapter,
|
||||
|
||||
Reference in New Issue
Block a user