diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 4a00dad41..aa773d0bd 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -183,6 +183,12 @@ "kb_final_top_k": 5, # 知识库检索最终返回结果数量 "kb_agentic_mode": False, "disable_builtin_commands": False, + "eula": { + "accepted": False, + "accepted_at": "", + "accepted_by": "", + "content_hash": "", + }, } @@ -2296,6 +2302,15 @@ class ChatProviderTemplate(TypedDict): "kb_fusion_top_k": {"type": "int", "default": 20}, "kb_final_top_k": {"type": "int", "default": 5}, "kb_agentic_mode": {"type": "bool"}, + "eula": { + "type": "object", + "items": { + "accepted": {"type": "bool"}, + "accepted_at": {"type": "string"}, + "accepted_by": {"type": "string"}, + "content_hash": {"type": "string"}, + }, + }, }, }, } diff --git a/astrbot/dashboard/routes/__init__.py b/astrbot/dashboard/routes/__init__.py index bca1a2268..e41884306 100644 --- a/astrbot/dashboard/routes/__init__.py +++ b/astrbot/dashboard/routes/__init__.py @@ -4,6 +4,7 @@ from .command import CommandRoute from .config import ConfigRoute from .conversation import ConversationRoute +from .eula import EulaRoute from .file import FileRoute from .knowledge_base import KnowledgeBaseRoute from .log import LogRoute @@ -23,6 +24,7 @@ "CommandRoute", "ConfigRoute", "ConversationRoute", + "EulaRoute", "FileRoute", "KnowledgeBaseRoute", "LogRoute", diff --git a/astrbot/dashboard/routes/eula.py b/astrbot/dashboard/routes/eula.py new file mode 100644 index 000000000..66b4c1a10 --- /dev/null +++ b/astrbot/dashboard/routes/eula.py @@ -0,0 +1,185 @@ +"""EULA (最终用户许可协议) 相关路由 + +提供 EULA 内容获取、签署状态查询和签署确认功能。 +签署状态存储在 cmd_config.json 中。 +支持通过 hash 检测 EULA 文件变化,如有变动则要求重新签署。 +""" + +import hashlib +import os +from datetime import datetime, timezone + +from quart import g + +from astrbot.core.utils.astrbot_path import get_astrbot_path, get_astrbot_root + +from .route import Response, Route, RouteContext + + +class EulaRoute(Route): + """EULA 路由类""" + + def __init__( + self, + context: RouteContext, + ) -> None: + super().__init__(context) + self.routes = { + "/eula/status": ("GET", self.get_eula_status), + "/eula/content": ("GET", self.get_eula_content), + "/eula/accept": ("POST", self.accept_eula), + } + self.register_routes() + + def _get_eula_file_path(self) -> str | None: + """获取 EULA 文件路径 + + Returns: + EULA 文件路径,如果不存在则返回 None + """ + eula_paths = [ + os.path.join(get_astrbot_path(), "EULA.md"), + os.path.join(get_astrbot_root(), "EULA.md"), + ] + for path in eula_paths: + if os.path.exists(path): + return path + return None + + def _calculate_eula_hash(self, file_path: str) -> str: + """计算 EULA 文件的 SHA256 哈希值 + + Args: + file_path: EULA 文件路径 + + Returns: + 文件内容的 SHA256 哈希值 + """ + with open(file_path, "rb") as f: + content = f.read() + return hashlib.sha256(content).hexdigest() + + def _get_current_eula_hash(self) -> str | None: + """获取当前 EULA 文件的哈希值 + + Returns: + 当前 EULA 文件的哈希值,如果文件不存在则返回 None + """ + file_path = self._get_eula_file_path() + if file_path is None: + return None + return self._calculate_eula_hash(file_path) + + async def get_eula_status(self): + """获取 EULA 签署状态 + + 从 cmd_config.json 中读取 eula 配置项,并检查 EULA 文件是否有变化。 + 如果 EULA 文件哈希值与签署时记录的不一致,则返回未签署状态。 + + Returns: + 包含 accepted 状态和签署时间的响应 + """ + try: + eula_config = self.config.get("eula", {}) + + if eula_config.get("accepted"): + # 检查 EULA 文件是否有变化 + current_hash = self._get_current_eula_hash() + stored_hash = eula_config.get("content_hash") + + # 如果哈希值不匹配,说明 EULA 已更新,需要重新签署 + if current_hash is not None and current_hash != stored_hash: + return ( + Response() + .ok( + { + "accepted": False, + "reason": "eula_updated", + } + ) + .__dict__ + ) + + return ( + Response() + .ok( + { + "accepted": True, + "accepted_at": eula_config.get("accepted_at"), + "accepted_by": eula_config.get("accepted_by"), + } + ) + .__dict__ + ) + + return Response().ok({"accepted": False}).__dict__ + except Exception as e: + return Response().error(f"获取 EULA 状态失败: {e!s}").__dict__ + + async def get_eula_content(self): + """获取 EULA 内容 + + Returns: + EULA markdown 内容 + """ + try: + # 尝试从不同位置读取 EULA.md + # 优先级: 项目源码目录 > 根目录 + eula_paths = [ + os.path.join(get_astrbot_path(), "EULA.md"), + os.path.join(get_astrbot_root(), "EULA.md"), + ] + + eula_content = None + for path in eula_paths: + if os.path.exists(path): + with open(path, encoding="utf-8") as f: + eula_content = f.read() + break + + if eula_content is None: + return Response().error("EULA 文件未找到").__dict__ + + return Response().ok({"content": eula_content}).__dict__ + except Exception as e: + return Response().error(f"获取 EULA 内容失败: {e!s}").__dict__ + + async def accept_eula(self): + """确认签署 EULA + + 将签署状态保存到 cmd_config.json 中,同时记录 EULA 文件的哈希值。 + 当 EULA 文件更新后,哈希值变化会触发重新签署。 + + Returns: + 签署结果 + """ + try: + username = getattr(g, "username", "unknown") + now = datetime.now(timezone.utc).isoformat() + + # 计算当前 EULA 文件的哈希值 + current_hash = self._get_current_eula_hash() + + # 更新配置,包含哈希值 + self.config["eula"] = { + "accepted": True, + "accepted_at": now, + "accepted_by": username, + "content_hash": current_hash, + } + self.config.save_config() + + return ( + Response() + .ok( + { + "accepted": True, + "accepted_at": now, + "accepted_by": username, + }, + "EULA 已签署", + ) + .__dict__ + ) + except Exception as e: + return Response().error(f"签署 EULA 失败: {e!s}").__dict__ diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index ad83c4886..d3429bba8 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -20,6 +20,7 @@ from .routes import * from .routes.backup import BackupRoute +from .routes.eula import EulaRoute from .routes.platform import PlatformRoute from .routes.route import Response, RouteContext from .routes.session_management import SessionManagementRoute @@ -87,6 +88,7 @@ def __init__( self.kb_route = KnowledgeBaseRoute(self.context, core_lifecycle) self.platform_route = PlatformRoute(self.context, core_lifecycle) self.backup_route = BackupRoute(self.context, db, core_lifecycle) + self.eula_route = EulaRoute(self.context) self.app.add_url_rule( "/api/plug/", diff --git a/dashboard/src/components/shared/EulaDialog.vue b/dashboard/src/components/shared/EulaDialog.vue new file mode 100644 index 000000000..5f49e6d22 --- /dev/null +++ b/dashboard/src/components/shared/EulaDialog.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/dashboard/src/i18n/loader.ts b/dashboard/src/i18n/loader.ts index ad17d58cb..c0fd7f7b9 100644 --- a/dashboard/src/i18n/loader.ts +++ b/dashboard/src/i18n/loader.ts @@ -56,6 +56,7 @@ export class I18nLoader { { name: 'features/alkaid/memory', path: 'features/alkaid/memory.json' }, { name: 'features/persona', path: 'features/persona.json' }, { name: 'features/migration', path: 'features/migration.json' }, + { name: 'features/eula', path: 'features/eula.json' }, // 消息模块 { name: 'messages/errors', path: 'messages/errors.json' }, diff --git a/dashboard/src/i18n/locales/en-US/features/eula.json b/dashboard/src/i18n/locales/en-US/features/eula.json new file mode 100644 index 000000000..9c4f2a84c --- /dev/null +++ b/dashboard/src/i18n/locales/en-US/features/eula.json @@ -0,0 +1,11 @@ +{ + "dialog": { + "title": "End User License Agreement (EULA)", + "loading": "Loading agreement content...", + "loadError": "Failed to load agreement content, please retry", + "retry": "Retry", + "acceptLabel": "I have read and agree to the above terms", + "confirm": "Confirm and Continue", + "acceptError": "Failed to accept the agreement, please retry" + } +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/eula.json b/dashboard/src/i18n/locales/zh-CN/features/eula.json new file mode 100644 index 000000000..2b582701e --- /dev/null +++ b/dashboard/src/i18n/locales/zh-CN/features/eula.json @@ -0,0 +1,11 @@ +{ + "dialog": { + "title": "最终用户许可协议 (EULA)", + "loading": "正在加载协议内容...", + "loadError": "加载协议内容失败,请重试", + "retry": "重试", + "acceptLabel": "我已阅读并同意上述协议内容", + "confirm": "确认并继续", + "acceptError": "签署协议失败,请重试" + } +} diff --git a/dashboard/src/i18n/translations.ts b/dashboard/src/i18n/translations.ts index 8cff882be..f1034a1b1 100644 --- a/dashboard/src/i18n/translations.ts +++ b/dashboard/src/i18n/translations.ts @@ -33,6 +33,7 @@ import zhCNKnowledgeBaseDocument from './locales/zh-CN/features/knowledge-base/d import zhCNPersona from './locales/zh-CN/features/persona.json'; import zhCNMigration from './locales/zh-CN/features/migration.json'; import zhCNCommand from './locales/zh-CN/features/command.json'; +import zhCNEula from './locales/zh-CN/features/eula.json'; import zhCNErrors from './locales/zh-CN/messages/errors.json'; import zhCNSuccess from './locales/zh-CN/messages/success.json'; @@ -70,6 +71,7 @@ import enUSKnowledgeBaseDocument from './locales/en-US/features/knowledge-base/d import enUSPersona from './locales/en-US/features/persona.json'; import enUSMigration from './locales/en-US/features/migration.json'; import enUSCommand from './locales/en-US/features/command.json'; +import enUSEula from './locales/en-US/features/eula.json'; import enUSErrors from './locales/en-US/messages/errors.json'; import enUSSuccess from './locales/en-US/messages/success.json'; @@ -114,7 +116,8 @@ export const translations = { }, persona: zhCNPersona, migration: zhCNMigration, - command: zhCNCommand + command: zhCNCommand, + eula: zhCNEula }, messages: { errors: zhCNErrors, @@ -159,7 +162,8 @@ export const translations = { }, persona: enUSPersona, migration: enUSMigration, - command: enUSCommand + command: enUSCommand, + eula: enUSEula }, messages: { errors: enUSErrors, diff --git a/dashboard/src/layouts/full/FullLayout.vue b/dashboard/src/layouts/full/FullLayout.vue index 7575eb56e..6c1662aca 100644 --- a/dashboard/src/layouts/full/FullLayout.vue +++ b/dashboard/src/layouts/full/FullLayout.vue @@ -5,6 +5,7 @@ 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 EulaDialog from '@/components/shared/EulaDialog.vue'; import Chat from '@/components/chat/Chat.vue'; import { useCustomizerStore } from '@/stores/customizer'; import { useRouterLoadingStore } from '@/stores/routerLoading'; @@ -29,6 +30,18 @@ const showChatPage = computed(() => { }); const migrationDialog = ref | null>(null); +const eulaDialog = ref | null>(null); + +// 检查 EULA 签署状态 +const checkEula = async () => { + try { + if (eulaDialog.value && typeof eulaDialog.value.checkAndOpen === 'function') { + await eulaDialog.value.checkAndOpen(); + } + } catch (error) { + console.error('Failed to check EULA status:', error); + } +}; // 检查是否需要迁移 const checkMigration = async () => { @@ -52,8 +65,11 @@ const checkMigration = async () => { }; onMounted(() => { - // 页面加载时检查是否需要迁移 - setTimeout(checkMigration, 1000); // 延迟1秒执行,确保页面完全加载 + // 页面加载时先检查 EULA,再检查迁移 + setTimeout(async () => { + await checkEula(); + await checkMigration(); + }, 1000); // 延迟1秒执行,确保页面完全加载 }); @@ -96,6 +112,9 @@ onMounted(() => { + + +