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 time
|
||||||
import traceback
|
import traceback
|
||||||
from functools import cmp_to_key
|
from functools import cmp_to_key
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import psutil
|
import psutil
|
||||||
@@ -37,6 +38,7 @@ class StatRoute(Route):
|
|||||||
"/stat/test-ghproxy-connection": ("POST", self.test_ghproxy_connection),
|
"/stat/test-ghproxy-connection": ("POST", self.test_ghproxy_connection),
|
||||||
"/stat/changelog": ("GET", self.get_changelog),
|
"/stat/changelog": ("GET", self.get_changelog),
|
||||||
"/stat/changelog/list": ("GET", self.list_changelog_versions),
|
"/stat/changelog/list": ("GET", self.list_changelog_versions),
|
||||||
|
"/stat/first-notice": ("GET", self.get_first_notice),
|
||||||
}
|
}
|
||||||
self.db_helper = db_helper
|
self.db_helper = db_helper
|
||||||
self.register_routes()
|
self.register_routes()
|
||||||
@@ -279,3 +281,39 @@ class StatRoute(Route):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return Response().error(f"Error: {e!s}").__dict__
|
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: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "readme",
|
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 modeConfig = computed(() => {
|
||||||
const isChangelog = props.mode === "changelog";
|
if (props.mode === "changelog") {
|
||||||
const keyBase = `core.common.${isChangelog ? "changelog" : "readme"}`;
|
|
||||||
return {
|
return {
|
||||||
title: t(`${keyBase}.title`),
|
title: t("core.common.changelog.title"),
|
||||||
loading: t(`${keyBase}.loading`),
|
loading: t("core.common.changelog.loading"),
|
||||||
emptyTitle: t(`${keyBase}.empty.title`),
|
emptyTitle: t("core.common.changelog.empty.title"),
|
||||||
emptySubtitle: t(`${keyBase}.empty.subtitle`),
|
emptySubtitle: t("core.common.changelog.empty.subtitle"),
|
||||||
apiPath: `/api/plugin/${isChangelog ? "changelog" : "readme"}`,
|
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("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() {
|
async function fetchContent() {
|
||||||
if (!props.pluginName) return;
|
if (requiresPluginName.value && !props.pluginName) return;
|
||||||
const requestId = ++lastRequestId.value;
|
const requestId = ++lastRequestId.value;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
content.value = null;
|
content.value = null;
|
||||||
@@ -186,9 +217,13 @@ async function fetchContent() {
|
|||||||
isEmpty.value = false;
|
isEmpty.value = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(
|
let params;
|
||||||
`${modeConfig.value.apiPath}?name=${props.pluginName}`,
|
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 (requestId !== lastRequestId.value) return;
|
||||||
|
|
||||||
if (res.data.status === "ok") {
|
if (res.data.status === "ok") {
|
||||||
@@ -207,7 +242,9 @@ async function fetchContent() {
|
|||||||
watch(
|
watch(
|
||||||
[() => props.show, () => props.pluginName, () => props.mode],
|
[() => props.show, () => props.pluginName, () => props.mode],
|
||||||
([show, name]) => {
|
([show, name]) => {
|
||||||
if (show && name) fetchContent();
|
if (!show) return;
|
||||||
|
if (requiresPluginName.value && !name) return;
|
||||||
|
fetchContent();
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -273,22 +310,26 @@ function openExternalLink(url) {
|
|||||||
if (!url) return;
|
if (!url) return;
|
||||||
window.open(url, "_blank", "noopener,noreferrer");
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showActionArea = computed(() => {
|
||||||
|
const hasGithub = modeConfig.value.showGithubButton && !!props.repoUrl;
|
||||||
|
return hasGithub || modeConfig.value.showRefreshButton;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-dialog v-model="_show" width="800">
|
<v-dialog v-model="_show" width="800">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="d-flex justify-space-between align-center">
|
<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-btn icon @click="_show = false" variant="text">
|
||||||
<v-icon>mdi-close</v-icon>
|
<v-icon>mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider></v-divider>
|
<v-card-text style="overflow-y: auto">
|
||||||
<v-card-text style="height: 70vh; overflow-y: auto">
|
<div v-if="showActionArea" class="d-flex justify-space-between mb-4">
|
||||||
<div class="d-flex justify-space-between mb-4">
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="repoUrl"
|
v-if="modeConfig.showGithubButton && repoUrl"
|
||||||
color="primary"
|
color="primary"
|
||||||
prepend-icon="mdi-github"
|
prepend-icon="mdi-github"
|
||||||
@click="openExternalLink(repoUrl)"
|
@click="openExternalLink(repoUrl)"
|
||||||
@@ -296,11 +337,12 @@ function openExternalLink(url) {
|
|||||||
{{ t("core.common.readme.buttons.viewOnGithub") }}
|
{{ t("core.common.readme.buttons.viewOnGithub") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
v-if="modeConfig.showRefreshButton"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
prepend-icon="mdi-refresh"
|
prepend-icon="mdi-refresh"
|
||||||
@click="fetchContent"
|
@click="fetchContent"
|
||||||
>
|
>
|
||||||
{{ t("core.common.readme.buttons.refresh") }}
|
{{ modeConfig.refreshLabel }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -357,7 +399,6 @@ function openExternalLink(url) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="primary" variant="tonal" @click="_show = false">
|
<v-btn color="primary" variant="tonal" @click="_show = false">
|
||||||
|
|||||||
@@ -104,5 +104,13 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"noData": "No data available"
|
"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": "编辑",
|
"edit": "编辑",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"noData": "暂无数据"
|
"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 VerticalSidebarVue from './vertical-sidebar/VerticalSidebar.vue';
|
||||||
import VerticalHeaderVue from './vertical-header/VerticalHeader.vue';
|
import VerticalHeaderVue from './vertical-header/VerticalHeader.vue';
|
||||||
import MigrationDialog from '@/components/shared/MigrationDialog.vue';
|
import MigrationDialog from '@/components/shared/MigrationDialog.vue';
|
||||||
|
import ReadmeDialog from '@/components/shared/ReadmeDialog.vue';
|
||||||
import Chat from '@/components/chat/Chat.vue';
|
import Chat from '@/components/chat/Chat.vue';
|
||||||
import { useCustomizerStore } from '@/stores/customizer';
|
import { useCustomizerStore } from '@/stores/customizer';
|
||||||
import { useRouterLoadingStore } from '@/stores/routerLoading';
|
import { useRouterLoadingStore } from '@/stores/routerLoading';
|
||||||
|
import { useI18n } from '@/i18n/composables';
|
||||||
|
|
||||||
|
const FIRST_NOTICE_SEEN_KEY = 'astrbot:first_notice_seen:v1';
|
||||||
|
|
||||||
const customizer = useCustomizerStore();
|
const customizer = useCustomizerStore();
|
||||||
|
const { locale } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const routerLoadingStore = useRouterLoadingStore();
|
const routerLoadingStore = useRouterLoadingStore();
|
||||||
|
|
||||||
// 计算是否在聊天页面(非全屏模式)
|
|
||||||
const isChatPage = computed(() => {
|
const isChatPage = computed(() => {
|
||||||
return route.path.startsWith('/chat');
|
return route.path.startsWith('/chat');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算是否显示 sidebar(仅在 bot 模式下显示)
|
|
||||||
const showSidebar = computed(() => {
|
const showSidebar = computed(() => {
|
||||||
return customizer.viewMode === 'bot';
|
return customizer.viewMode === 'bot';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算是否显示 chat 页面(在 chat 模式下显示)
|
|
||||||
const showChatPage = computed(() => {
|
const showChatPage = computed(() => {
|
||||||
return customizer.viewMode === 'chat';
|
return customizer.viewMode === 'chat';
|
||||||
});
|
});
|
||||||
|
|
||||||
const migrationDialog = ref<InstanceType<typeof MigrationDialog> | null>(null);
|
const migrationDialog = ref<InstanceType<typeof MigrationDialog> | null>(null);
|
||||||
|
const showFirstNoticeDialog = ref(false);
|
||||||
|
|
||||||
// 检查是否需要迁移
|
const checkMigration = async (): Promise<boolean> => {
|
||||||
const checkMigration = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/stat/version');
|
const response = await axios.get('/api/stat/version');
|
||||||
if (response.data.status === 'ok' && response.data.data.need_migration) {
|
if (response.data.status === 'ok' && response.data.data.need_migration) {
|
||||||
// 需要迁移,显示迁移对话框
|
|
||||||
if (migrationDialog.value && typeof migrationDialog.value.open === 'function') {
|
if (migrationDialog.value && typeof migrationDialog.value.open === 'function') {
|
||||||
const result = await migrationDialog.value.open();
|
const result = await migrationDialog.value.open();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 迁移成功,可以显示成功消息
|
|
||||||
console.log('Migration completed successfully:', result.message);
|
console.log('Migration completed successfully:', result.message);
|
||||||
// 可以考虑刷新页面或显示成功通知
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to check migration status:', 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(() => {
|
onMounted(() => {
|
||||||
// 页面加载时检查是否需要迁移
|
setTimeout(async () => {
|
||||||
setTimeout(checkMigration, 1000); // 延迟1秒执行,确保页面完全加载
|
const migrationPending = await checkMigration();
|
||||||
|
if (!migrationPending) {
|
||||||
|
await maybeShowFirstNotice();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -62,7 +99,6 @@ onMounted(() => {
|
|||||||
<v-app :theme="useCustomizerStore().uiTheme"
|
<v-app :theme="useCustomizerStore().uiTheme"
|
||||||
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
|
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
|
||||||
>
|
>
|
||||||
<!-- 路由切换进度条 -->
|
|
||||||
<v-progress-linear
|
<v-progress-linear
|
||||||
v-if="routerLoadingStore.isLoading"
|
v-if="routerLoadingStore.isLoading"
|
||||||
:model-value="routerLoadingStore.progress"
|
:model-value="routerLoadingStore.progress"
|
||||||
@@ -96,8 +132,12 @@ onMounted(() => {
|
|||||||
</v-container>
|
</v-container>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|
||||||
<!-- Migration Dialog -->
|
|
||||||
<MigrationDialog ref="migrationDialog" />
|
<MigrationDialog ref="migrationDialog" />
|
||||||
|
<ReadmeDialog
|
||||||
|
:show="showFirstNoticeDialog"
|
||||||
|
mode="first-notice"
|
||||||
|
@update:show="onFirstNoticeDialogUpdate"
|
||||||
|
/>
|
||||||
</v-app>
|
</v-app>
|
||||||
</v-locale-provider>
|
</v-locale-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user