feat: add first notice feature with multilingual support and UI integration
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
## Welcome to AstrBot
|
||||
|
||||
🌟 Thank you for using AstrBot!
|
||||
|
||||
AstrBot is an Agentic AI assistant for personal and group chats, with support for multiple IM platforms and a wide range of built-in features. We hope it brings you an efficient and enjoyable experience. ❤️
|
||||
|
||||
Important notice:
|
||||
|
||||
AstrBot is a **free and open-source software project** protected by the AGPLv3 license. You can find the full source code and related resources on our [**official website**](https://astrbot.app) and [**GitHub**](https://github.com/astrbotdevs/astrbot).
|
||||
As of now, AstrBot has **no commercial services of any kind**, and the official team **will never charge users any fees** under any name.
|
||||
|
||||
If anyone asks you to pay while using AstrBot, **you are likely being scammed**. Please request a refund immediately and report it to us by email.
|
||||
|
||||
📮 Official email: [community@astrbot.app](mailto:community@astrbot.app)
|
||||
@@ -0,0 +1,14 @@
|
||||
## 欢迎使用 AstrBot
|
||||
|
||||
🌟 感谢您使用 AstrBot!
|
||||
|
||||
AstrBot 是一款可接入多种 IM 平台的 Agentic AI 个人 / 群聊助手,内置多项强大功能,希望能为您带来高效、愉快的使用体验。❤️
|
||||
|
||||
我们想特别说明:
|
||||
|
||||
AstrBot 是受 AGPLv3 开源协议保护的**免费开源软件项目**,您可以在[**官方网站**](https://astrbot.app)、[**GitHub**](https://github.com/astrbotdevs/astrbot) 上找到 AstrBot 的全部源代码及相关资源。
|
||||
截至目前,AstrBot 项目**未开展任何形式的商业化服务**,官方**不会以任何名义向用户收取费用**。
|
||||
|
||||
如果您在使用 AstrBot 的过程中被要求付费,**表明您已经遭遇诈骗行为**。请立即向相关方申请退款,并及时通过邮件向我们反馈。
|
||||
|
||||
📮 官方邮箱:[community@astrbot.app](mailto:community@astrbot.app)
|
||||
@@ -4,6 +4,7 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
from functools import cmp_to_key
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
import psutil
|
||||
@@ -37,6 +38,7 @@ class StatRoute(Route):
|
||||
"/stat/test-ghproxy-connection": ("POST", self.test_ghproxy_connection),
|
||||
"/stat/changelog": ("GET", self.get_changelog),
|
||||
"/stat/changelog/list": ("GET", self.list_changelog_versions),
|
||||
"/stat/first-notice": ("GET", self.get_first_notice),
|
||||
}
|
||||
self.db_helper = db_helper
|
||||
self.register_routes()
|
||||
@@ -279,3 +281,39 @@ class StatRoute(Route):
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"Error: {e!s}").__dict__
|
||||
|
||||
async def get_first_notice(self):
|
||||
"""读取项目根目录 FIRST_NOTICE.md 内容。"""
|
||||
try:
|
||||
locale = (request.args.get("locale") or "").strip()
|
||||
if not re.match(r"^[A-Za-z0-9_-]*$", locale):
|
||||
locale = ""
|
||||
|
||||
base_path = Path(get_astrbot_path())
|
||||
candidates: list[Path] = []
|
||||
|
||||
if locale:
|
||||
candidates.append(base_path / f"FIRST_NOTICE.{locale}.md")
|
||||
if locale.lower().startswith("zh"):
|
||||
candidates.append(base_path / "FIRST_NOTICE.zh-CN.md")
|
||||
elif locale.lower().startswith("en"):
|
||||
candidates.append(base_path / "FIRST_NOTICE.en-US.md")
|
||||
|
||||
candidates.extend(
|
||||
[
|
||||
base_path / "FIRST_NOTICE.en-US.md",
|
||||
base_path / "FIRST_NOTICE.md",
|
||||
],
|
||||
)
|
||||
|
||||
for notice_path in candidates:
|
||||
if not notice_path.is_file():
|
||||
continue
|
||||
content = notice_path.read_text(encoding="utf-8")
|
||||
if content.strip():
|
||||
return Response().ok({"content": content}).__dict__
|
||||
|
||||
return Response().ok({"content": None}).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"Error: {e!s}").__dict__
|
||||
|
||||
@@ -35,7 +35,7 @@ const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: "readme",
|
||||
validator: (value) => ["readme", "changelog"].includes(value),
|
||||
validator: (value) => ["readme", "changelog", "first-notice"].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -166,19 +166,50 @@ const renderedHtml = computed(() => {
|
||||
});
|
||||
|
||||
const modeConfig = computed(() => {
|
||||
const isChangelog = props.mode === "changelog";
|
||||
const keyBase = `core.common.${isChangelog ? "changelog" : "readme"}`;
|
||||
if (props.mode === "changelog") {
|
||||
return {
|
||||
title: t("core.common.changelog.title"),
|
||||
loading: t("core.common.changelog.loading"),
|
||||
emptyTitle: t("core.common.changelog.empty.title"),
|
||||
emptySubtitle: t("core.common.changelog.empty.subtitle"),
|
||||
apiPath: "/api/plugin/changelog",
|
||||
showGithubButton: false,
|
||||
showRefreshButton: true,
|
||||
refreshLabel: t("core.common.readme.buttons.refresh"),
|
||||
};
|
||||
}
|
||||
|
||||
if (props.mode === "first-notice") {
|
||||
return {
|
||||
title: t("core.common.firstNotice.title"),
|
||||
loading: t("core.common.firstNotice.loading"),
|
||||
emptyTitle: t("core.common.firstNotice.empty.title"),
|
||||
emptySubtitle: t("core.common.firstNotice.empty.subtitle"),
|
||||
apiPath: "/api/stat/first-notice",
|
||||
showGithubButton: false,
|
||||
showRefreshButton: false,
|
||||
refreshLabel: "",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: t(`${keyBase}.title`),
|
||||
loading: t(`${keyBase}.loading`),
|
||||
emptyTitle: t(`${keyBase}.empty.title`),
|
||||
emptySubtitle: t(`${keyBase}.empty.subtitle`),
|
||||
apiPath: `/api/plugin/${isChangelog ? "changelog" : "readme"}`,
|
||||
title: t("core.common.readme.title"),
|
||||
loading: t("core.common.readme.loading"),
|
||||
emptyTitle: t("core.common.readme.empty.title"),
|
||||
emptySubtitle: t("core.common.readme.empty.subtitle"),
|
||||
apiPath: "/api/plugin/readme",
|
||||
showGithubButton: true,
|
||||
showRefreshButton: true,
|
||||
refreshLabel: t("core.common.readme.buttons.refresh"),
|
||||
};
|
||||
});
|
||||
|
||||
const requiresPluginName = computed(
|
||||
() => props.mode === "readme" || props.mode === "changelog",
|
||||
);
|
||||
|
||||
async function fetchContent() {
|
||||
if (!props.pluginName) return;
|
||||
if (requiresPluginName.value && !props.pluginName) return;
|
||||
const requestId = ++lastRequestId.value;
|
||||
loading.value = true;
|
||||
content.value = null;
|
||||
@@ -186,9 +217,13 @@ async function fetchContent() {
|
||||
isEmpty.value = false;
|
||||
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`${modeConfig.value.apiPath}?name=${props.pluginName}`,
|
||||
);
|
||||
let params;
|
||||
if (requiresPluginName.value) {
|
||||
params = { name: props.pluginName };
|
||||
} else if (props.mode === "first-notice") {
|
||||
params = { locale: locale.value };
|
||||
}
|
||||
const res = await axios.get(modeConfig.value.apiPath, { params });
|
||||
if (requestId !== lastRequestId.value) return;
|
||||
|
||||
if (res.data.status === "ok") {
|
||||
@@ -207,7 +242,9 @@ async function fetchContent() {
|
||||
watch(
|
||||
[() => props.show, () => props.pluginName, () => props.mode],
|
||||
([show, name]) => {
|
||||
if (show && name) fetchContent();
|
||||
if (!show) return;
|
||||
if (requiresPluginName.value && !name) return;
|
||||
fetchContent();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -273,22 +310,26 @@ function openExternalLink(url) {
|
||||
if (!url) return;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
const showActionArea = computed(() => {
|
||||
const hasGithub = modeConfig.value.showGithubButton && !!props.repoUrl;
|
||||
return hasGithub || modeConfig.value.showRefreshButton;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="_show" width="800">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h5">{{ modeConfig.title }}</span>
|
||||
<span class="text-h2 pa-2">{{ modeConfig.title }}</span>
|
||||
<v-btn icon @click="_show = false" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="height: 70vh; overflow-y: auto">
|
||||
<div class="d-flex justify-space-between mb-4">
|
||||
<v-card-text style="overflow-y: auto">
|
||||
<div v-if="showActionArea" class="d-flex justify-space-between mb-4">
|
||||
<v-btn
|
||||
v-if="repoUrl"
|
||||
v-if="modeConfig.showGithubButton && repoUrl"
|
||||
color="primary"
|
||||
prepend-icon="mdi-github"
|
||||
@click="openExternalLink(repoUrl)"
|
||||
@@ -296,11 +337,12 @@ function openExternalLink(url) {
|
||||
{{ t("core.common.readme.buttons.viewOnGithub") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="modeConfig.showRefreshButton"
|
||||
color="secondary"
|
||||
prepend-icon="mdi-refresh"
|
||||
@click="fetchContent"
|
||||
>
|
||||
{{ t("core.common.readme.buttons.refresh") }}
|
||||
{{ modeConfig.refreshLabel }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -357,7 +399,6 @@ function openExternalLink(url) {
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="tonal" @click="_show = false">
|
||||
|
||||
@@ -104,5 +104,13 @@
|
||||
"edit": "Edit",
|
||||
"copy": "Copy",
|
||||
"noData": "No data available"
|
||||
},
|
||||
"firstNotice": {
|
||||
"title": "First Notice",
|
||||
"loading": "Loading first notice...",
|
||||
"empty": {
|
||||
"title": "No first notice content available",
|
||||
"subtitle": "FIRST_NOTICE.md was not found or is empty."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,5 +104,13 @@
|
||||
"edit": "编辑",
|
||||
"copy": "复制",
|
||||
"noData": "暂无数据"
|
||||
},
|
||||
"firstNotice": {
|
||||
"title": "首次提示",
|
||||
"loading": "正在加载首次提示...",
|
||||
"empty": {
|
||||
"title": "暂无首次提示内容",
|
||||
"subtitle": "未找到 FIRST_NOTICE.md 或文件为空。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,55 +5,92 @@ import axios from 'axios';
|
||||
import VerticalSidebarVue from './vertical-sidebar/VerticalSidebar.vue';
|
||||
import VerticalHeaderVue from './vertical-header/VerticalHeader.vue';
|
||||
import MigrationDialog from '@/components/shared/MigrationDialog.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 { useI18n } from '@/i18n/composables';
|
||||
|
||||
const FIRST_NOTICE_SEEN_KEY = 'astrbot:first_notice_seen:v1';
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const { locale } = useI18n();
|
||||
const route = useRoute();
|
||||
const routerLoadingStore = useRouterLoadingStore();
|
||||
|
||||
// 计算是否在聊天页面(非全屏模式)
|
||||
const isChatPage = computed(() => {
|
||||
return route.path.startsWith('/chat');
|
||||
});
|
||||
|
||||
// 计算是否显示 sidebar(仅在 bot 模式下显示)
|
||||
const showSidebar = computed(() => {
|
||||
return customizer.viewMode === 'bot';
|
||||
});
|
||||
|
||||
// 计算是否显示 chat 页面(在 chat 模式下显示)
|
||||
const showChatPage = computed(() => {
|
||||
return customizer.viewMode === 'chat';
|
||||
});
|
||||
|
||||
const migrationDialog = ref<InstanceType<typeof MigrationDialog> | null>(null);
|
||||
const showFirstNoticeDialog = ref(false);
|
||||
|
||||
// 检查是否需要迁移
|
||||
const checkMigration = async () => {
|
||||
const checkMigration = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await axios.get('/api/stat/version');
|
||||
if (response.data.status === 'ok' && response.data.data.need_migration) {
|
||||
// 需要迁移,显示迁移对话框
|
||||
if (migrationDialog.value && typeof migrationDialog.value.open === 'function') {
|
||||
const result = await migrationDialog.value.open();
|
||||
if (result.success) {
|
||||
// 迁移成功,可以显示成功消息
|
||||
console.log('Migration completed successfully:', result.message);
|
||||
// 可以考虑刷新页面或显示成功通知
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check migration status:', error);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const maybeShowFirstNotice = async () => {
|
||||
if (localStorage.getItem(FIRST_NOTICE_SEEN_KEY) === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/stat/first-notice', {
|
||||
params: { locale: locale.value },
|
||||
});
|
||||
if (response.data.status !== 'ok') {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = response.data?.data?.content;
|
||||
if (typeof content === 'string' && content.trim().length > 0) {
|
||||
showFirstNoticeDialog.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(FIRST_NOTICE_SEEN_KEY, '1');
|
||||
} catch (error) {
|
||||
console.error('Failed to load first notice:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const onFirstNoticeDialogUpdate = (visible: boolean) => {
|
||||
showFirstNoticeDialog.value = visible;
|
||||
if (!visible) {
|
||||
localStorage.setItem(FIRST_NOTICE_SEEN_KEY, '1');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 页面加载时检查是否需要迁移
|
||||
setTimeout(checkMigration, 1000); // 延迟1秒执行,确保页面完全加载
|
||||
setTimeout(async () => {
|
||||
const migrationPending = await checkMigration();
|
||||
if (!migrationPending) {
|
||||
await maybeShowFirstNotice();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -62,7 +99,6 @@ onMounted(() => {
|
||||
<v-app :theme="useCustomizerStore().uiTheme"
|
||||
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
|
||||
>
|
||||
<!-- 路由切换进度条 -->
|
||||
<v-progress-linear
|
||||
v-if="routerLoadingStore.isLoading"
|
||||
:model-value="routerLoadingStore.progress"
|
||||
@@ -74,15 +110,15 @@ onMounted(() => {
|
||||
/>
|
||||
<VerticalHeaderVue />
|
||||
<VerticalSidebarVue v-if="showSidebar" />
|
||||
<v-main :style="{
|
||||
<v-main :style="{
|
||||
height: showChatPage ? 'calc(100vh - 55px)' : undefined,
|
||||
overflow: showChatPage ? 'hidden' : undefined
|
||||
}">
|
||||
<v-container
|
||||
fluid
|
||||
class="page-wrapper"
|
||||
<v-container
|
||||
fluid
|
||||
class="page-wrapper"
|
||||
:class="{ 'chat-mode-container': showChatPage }"
|
||||
:style="{
|
||||
:style="{
|
||||
height: showChatPage ? '100%' : 'calc(100% - 8px)',
|
||||
padding: (isChatPage || showChatPage) ? '0' : undefined,
|
||||
minHeight: showChatPage ? 'unset' : undefined
|
||||
@@ -95,9 +131,13 @@ onMounted(() => {
|
||||
</div>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<!-- Migration Dialog -->
|
||||
|
||||
<MigrationDialog ref="migrationDialog" />
|
||||
<ReadmeDialog
|
||||
:show="showFirstNoticeDialog"
|
||||
mode="first-notice"
|
||||
@update:show="onFirstNoticeDialogUpdate"
|
||||
/>
|
||||
</v-app>
|
||||
</v-locale-provider>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user