From cd7755fe071d119117bf2a44f8a4ca0960922831 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 12 Feb 2026 00:00:53 +0800 Subject: [PATCH] feat: add first notice feature with multilingual support and UI integration --- FIRST_NOTICE.en-US.md | 14 ++++ FIRST_NOTICE.md | 14 ++++ astrbot/dashboard/routes/stat.py | 38 +++++++++ .../src/components/shared/ReadmeDialog.vue | 81 ++++++++++++++----- .../src/i18n/locales/en-US/core/common.json | 8 ++ .../src/i18n/locales/zh-CN/core/common.json | 8 ++ dashboard/src/layouts/full/FullLayout.vue | 76 ++++++++++++----- 7 files changed, 201 insertions(+), 38 deletions(-) create mode 100644 FIRST_NOTICE.en-US.md create mode 100644 FIRST_NOTICE.md diff --git a/FIRST_NOTICE.en-US.md b/FIRST_NOTICE.en-US.md new file mode 100644 index 000000000..ba717b5ef --- /dev/null +++ b/FIRST_NOTICE.en-US.md @@ -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) diff --git a/FIRST_NOTICE.md b/FIRST_NOTICE.md new file mode 100644 index 000000000..bc739ed73 --- /dev/null +++ b/FIRST_NOTICE.md @@ -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) diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 054eec995..666eb4c83 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -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__ diff --git a/dashboard/src/components/shared/ReadmeDialog.vue b/dashboard/src/components/shared/ReadmeDialog.vue index 04ab7afd1..ddc27cd90 100644 --- a/dashboard/src/components/shared/ReadmeDialog.vue +++ b/dashboard/src/components/shared/ReadmeDialog.vue @@ -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; +});