mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat(i18n): add Chinese and English language support for CLI and TUI
- Add CLI i18n module (astrbot/cli/i18n.py) with zh/en translations - Add TUI i18n module (astrbot/tui/i18n.py) with zh/en translations - Update CLI commands to use translated strings - Update TUI app and screen to use translated strings - Add ASTRBOT_CLI_LANG and ASTRBOT_TUI_LANG to .env.example - Update cmd_run.py env var documentation - Add backward compatibility for moved modules with DeprecationWarning
This commit is contained in:
16
.env.example
16
.env.example
@@ -80,6 +80,22 @@ ASTRBOT_SYSTEMD=1
|
||||
# 默认 Default: True
|
||||
ASTRBOT_DASHBOARD_ENABLE=True
|
||||
|
||||
# ------------------------------------------
|
||||
# 国际化配置 / Internationalization Configuration
|
||||
# ------------------------------------------
|
||||
|
||||
# CLI 界面语言
|
||||
# CLI interface language
|
||||
# 可选值 Values: zh (中文), en (英文)
|
||||
# 默认 Default: zh (跟随系统 locale / follows system locale)
|
||||
# ASTRBOT_CLI_LANG=zh
|
||||
|
||||
# TUI 界面语言
|
||||
# TUI interface language
|
||||
# 可选值 Values: zh (中文), en (英文)
|
||||
# 默认 Default: zh
|
||||
# ASTRBOT_TUI_LANG=zh
|
||||
|
||||
# ------------------------------------------
|
||||
# 网络配置 / Network Configuration
|
||||
# ------------------------------------------
|
||||
|
||||
@@ -8,6 +8,7 @@ from click.shell_completion import get_completion_class
|
||||
|
||||
from . import __version__
|
||||
from .commands import bk, conf, init, plug, run, tui, uninstall
|
||||
from .i18n import t
|
||||
|
||||
logo_tmpl = r"""
|
||||
___ _______.___________..______ .______ ______ .___________.
|
||||
@@ -26,8 +27,8 @@ def cli() -> None:
|
||||
Agentic IM Chatbot infrastructure that integrates lots of IM platforms, LLMs, plugins and AI feature, and can be your openclaw alternative. ✨
|
||||
"""
|
||||
click.echo(logo_tmpl)
|
||||
click.echo("Welcome to AstrBot CLI!")
|
||||
click.echo(f"AstrBot CLI version: {__version__}")
|
||||
click.echo(t("cli_welcome"))
|
||||
click.echo(t("cli_version", version=__version__))
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -70,7 +71,7 @@ def help(command_name: str | None, all: bool) -> None:
|
||||
cmd_ctx = click.Context(command, info_name=command.name, parent=parent)
|
||||
click.echo(command.get_help(cmd_ctx))
|
||||
else:
|
||||
click.echo(f"Unknown command: {command_name}")
|
||||
click.echo(t("cli_unknown_command", command=command_name))
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Display general help information
|
||||
|
||||
@@ -3,6 +3,7 @@ import shutil
|
||||
|
||||
import click
|
||||
|
||||
from astrbot.cli.i18n import t
|
||||
from astrbot.cli.utils import (
|
||||
PluginStatus,
|
||||
build_plug_list,
|
||||
@@ -43,7 +44,7 @@ def new(name: str) -> None:
|
||||
plug_path = base_path / "plugins" / name
|
||||
|
||||
if plug_path.exists():
|
||||
raise click.ClickException(f"Plugin {name} already exists")
|
||||
raise click.ClickException(t("plugin_already_exists", name=name))
|
||||
|
||||
author = click.prompt("Enter plugin author", type=str)
|
||||
desc = click.prompt("Enter plugin description", type=str)
|
||||
@@ -155,7 +156,7 @@ def install(name: str, proxy: str | None) -> None:
|
||||
)
|
||||
|
||||
if not plugin:
|
||||
raise click.ClickException(f"Plugin {name} not found or already installed")
|
||||
raise click.ClickException(t("plugin_not_found_or_installed", name=name))
|
||||
|
||||
manage_plugin(plugin, plug_path, is_update=False, proxy=proxy)
|
||||
|
||||
@@ -171,19 +172,19 @@ def remove(name: str) -> None:
|
||||
plugin = next((p for p in plugins if p["name"] == name), None)
|
||||
|
||||
if not plugin or not plugin.get("local_path"):
|
||||
raise click.ClickException(f"Plugin {name} does not exist or is not installed")
|
||||
raise click.ClickException(t("plugin_not_found_or_installed", name=name))
|
||||
|
||||
plugin_path = plugin["local_path"]
|
||||
|
||||
click.confirm(
|
||||
f"Are you sure you want to uninstall plugin {name}?", default=False, abort=True
|
||||
t("plugin_uninstall_confirm", name=name), default=False, abort=True
|
||||
)
|
||||
|
||||
try:
|
||||
shutil.rmtree(plugin_path)
|
||||
click.echo(f"Plugin {name} has been uninstalled")
|
||||
click.echo(t("plugin_uninstall_success", name=name))
|
||||
except Exception as e:
|
||||
raise click.ClickException(f"Failed to uninstall plugin {name}: {e}")
|
||||
raise click.ClickException(t("plugin_uninstall_failed_ex", name=name, error=str(e)))
|
||||
|
||||
|
||||
@plug.command()
|
||||
@@ -219,13 +220,13 @@ def update(name: str, proxy: str | None) -> None:
|
||||
]
|
||||
|
||||
if not need_update_plugins:
|
||||
click.echo("No plugins need updating")
|
||||
click.echo(t("plugin_no_update_needed"))
|
||||
return
|
||||
|
||||
click.echo(f"Found {len(need_update_plugins)} plugin(s) needing update")
|
||||
click.echo(t("plugin_found_update", count=str(len(need_update_plugins))))
|
||||
for plugin in need_update_plugins:
|
||||
plugin_name = plugin["name"]
|
||||
click.echo(f"Updating plugin {plugin_name}...")
|
||||
click.echo(t("plugin_updating", name=plugin_name))
|
||||
manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
|
||||
|
||||
|
||||
@@ -247,7 +248,7 @@ def search(query: str) -> None:
|
||||
]
|
||||
|
||||
if not matched_plugins:
|
||||
click.echo(f"No plugins matching '{query}' found")
|
||||
click.echo(t("plugin_search_no_result", query=query))
|
||||
return
|
||||
|
||||
display_plugins(matched_plugins, f"Search results: '{query}'", "cyan")
|
||||
display_plugins(matched_plugins, t("plugin_search_results", query=query), "cyan")
|
||||
|
||||
@@ -14,23 +14,24 @@ Core:
|
||||
- `PYTHON`: Python executable path override (for local code execution).
|
||||
|
||||
Dashboard / Backend:
|
||||
- `ASTRBOT_DASHBOARD_ENABLE` / `DASHBOARD_ENABLE`: Enable/Disable Dashboard.
|
||||
- `ASTRBOT_HOST` / `DASHBOARD_HOST`: Dashboard bind host.
|
||||
- `ASTRBOT_PORT` / `DASHBOARD_PORT`: Dashboard bind port.
|
||||
- `ASTRBOT_DASHBOARD_ENABLE`: Enable/Disable Dashboard.
|
||||
- `ASTRBOT_HOST`: Dashboard bind host.
|
||||
- `ASTRBOT_PORT`: Dashboard bind port.
|
||||
|
||||
Backend-standard SSL names (preferred for server):
|
||||
- `ASTRBOT_SSL_ENABLE` / `DASHBOARD_SSL_ENABLE`: Enable SSL for API.
|
||||
- `ASTRBOT_SSL_CERT` / `DASHBOARD_SSL_CERT`: SSL Certificate path for backend.
|
||||
- `ASTRBOT_SSL_KEY` / `DASHBOARD_SSL_KEY`: SSL Key path for backend.
|
||||
- `ASTRBOT_SSL_CA_CERTS` / `DASHBOARD_SSL_CA_CERTS`: SSL CA Certs path for backend.
|
||||
|
||||
Legacy compatibility:
|
||||
- The CLI will set both `ASTRBOT_SSL_*` and the legacy `DASHBOARD_SSL_*` names to remain compatible.
|
||||
SSL (AstrBot-standard names):
|
||||
- `ASTRBOT_SSL_ENABLE`: Enable SSL for API.
|
||||
- `ASTRBOT_SSL_CERT`: SSL Certificate path for backend.
|
||||
- `ASTRBOT_SSL_KEY`: SSL Key path for backend.
|
||||
- `ASTRBOT_SSL_CA_CERTS`: SSL CA Certs path for backend.
|
||||
|
||||
Network:
|
||||
- `http_proxy` / `https_proxy`: Proxy URL.
|
||||
- `no_proxy`: No proxy list.
|
||||
|
||||
Internationalization:
|
||||
- `ASTRBOT_CLI_LANG`: CLI interface language (zh/en).
|
||||
- `ASTRBOT_TUI_LANG`: TUI interface language (zh/en).
|
||||
|
||||
Integrations:
|
||||
- `DASHSCOPE_API_KEY`: Alibaba DashScope API Key (for Rerank).
|
||||
- `COZE_API_KEY` / `COZE_BOT_ID`: Coze integration.
|
||||
|
||||
301
astrbot/cli/i18n.py
Normal file
301
astrbot/cli/i18n.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""Internationalization support for AstrBot CLI.
|
||||
|
||||
This module provides i18n support with Chinese and English languages.
|
||||
Language is auto-detected from environment or can be set manually.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Language(Enum):
|
||||
"""Supported languages."""
|
||||
|
||||
ZH = "zh"
|
||||
EN = "en"
|
||||
|
||||
|
||||
# Translation dictionaries
|
||||
_TRANSLATIONS: dict[Language, dict[str, str]] = {
|
||||
Language.ZH: {
|
||||
# CLI welcome and general
|
||||
"cli_welcome": "欢迎使用 AstrBot CLI!",
|
||||
"cli_version": "AstrBot CLI 版本: {version}",
|
||||
"cli_unknown_command": "未知命令: {command}",
|
||||
"cli_help_available": "使用 astrbot help --all 查看所有命令",
|
||||
|
||||
# Dashboard commands
|
||||
"dashboard_bundled": "Dashboard 已打包在安装包中 - 跳过下载",
|
||||
"dashboard_not_installed": "Dashboard 未安装",
|
||||
"dashboard_install_confirm": "是否安装 Dashboard?",
|
||||
"dashboard_installing": "正在安装 Dashboard...",
|
||||
"dashboard_install_success": "Dashboard 安装成功",
|
||||
"dashboard_install_failed": "Dashboard 安装失败: {error}",
|
||||
"dashboard_not_needed": "Dashboard 不需要安装",
|
||||
"dashboard_declined": "Dashboard 安装已取消",
|
||||
"dashboard_already_up_to_date": "Dashboard 已是最新版本",
|
||||
"dashboard_version": "Dashboard 版本: {version}",
|
||||
"dashboard_download_failed": "Dashboard 下载失败: {error}",
|
||||
"dashboard_init_dir": "正在初始化 Dashboard 目录...",
|
||||
"dashboard_init_success": "Dashboard 初始化成功",
|
||||
|
||||
# Plugin commands
|
||||
"plugin_installing": "正在安装插件: {name}",
|
||||
"plugin_install_success": "插件安装成功: {name}",
|
||||
"plugin_install_failed": "插件安装失败: {name}",
|
||||
"plugin_uninstall_confirm": "确定要卸载插件 {name} 吗?",
|
||||
"plugin_uninstall_success": "插件卸载成功: {name}",
|
||||
"plugin_uninstall_failed": "插件卸载失败: {name}",
|
||||
"plugin_list_empty": "未安装任何插件",
|
||||
"plugin_already_installed": "插件已安装: {name}",
|
||||
"plugin_not_found": "插件未找到: {name}",
|
||||
"plugin_already_exists": "插件已存在: {name}",
|
||||
"plugin_not_found_or_installed": "插件未找到或已安装: {name}",
|
||||
"plugin_uninstall_failed_ex": "插件卸载失败 {name}: {error}",
|
||||
"plugin_no_update_needed": "没有需要更新的插件",
|
||||
"plugin_found_update": "发现 {count} 个插件需要更新",
|
||||
"plugin_updating": "正在更新插件 {name}...",
|
||||
"plugin_search_no_result": "未找到匹配 '{query}' 的插件",
|
||||
"plugin_search_results": "搜索结果: '{query}'",
|
||||
|
||||
# Config commands
|
||||
"config_show": "显示配置",
|
||||
"config_set_success": "配置项已更新: {key} = {value}",
|
||||
"config_set_failed": "配置项更新失败: {key}",
|
||||
"config_set_failed_ex": "设置配置失败: {error}",
|
||||
"config_get_success": "{key} = {value}",
|
||||
"config_get_not_found": "配置项未找到: {key}",
|
||||
"config_reset_confirm": "确定要重置所有配置吗?",
|
||||
"config_reset_success": "配置已重置",
|
||||
|
||||
# Config validators
|
||||
"config_log_level_invalid": "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一",
|
||||
"config_port_must_be_number": "端口必须是数字",
|
||||
"config_port_range_invalid": "端口必须在 1-65535 范围内",
|
||||
"config_username_empty": "用户名不能为空",
|
||||
"config_password_empty": "密码不能为空",
|
||||
"config_timezone_invalid": "无效的时区: {value}。请使用有效的 IANA 时区名称",
|
||||
"config_callback_invalid": "回调 API 基础路径必须以 http:// 或 https:// 开头",
|
||||
"config_key_unsupported": "不支持的配置项: {key}",
|
||||
"config_key_unknown": "未知的配置项: {key}",
|
||||
"config_updated": "配置已更新: {key}",
|
||||
|
||||
# Init command
|
||||
"init_creating": "正在创建配置目录...",
|
||||
"init_created": "配置目录已创建: {path}",
|
||||
"init_copying": "正在复制配置文件...",
|
||||
"init_copied": "配置文件已复制",
|
||||
"init_success": "AstrBot 初始化完成!",
|
||||
"init_failed": "初始化失败: {error}",
|
||||
|
||||
# Run command
|
||||
"run_starting": "正在启动 AstrBot...",
|
||||
"run_started": "AstrBot 已启动!",
|
||||
"run_backend_only": "以无界面模式启动",
|
||||
"run_failed": "启动失败: {error}",
|
||||
"run_stopped": "AstrBot 已停止",
|
||||
|
||||
# TUI command
|
||||
"tui_starting": "正在启动 TUI...",
|
||||
"tui_started": "TUI 已启动",
|
||||
"tui_failed": "TUI 启动失败: {error}",
|
||||
|
||||
# Common
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"loading": "加载中...",
|
||||
"done": "完成",
|
||||
"failed": "失败",
|
||||
"retry": "重试",
|
||||
"exit": "退出",
|
||||
"continue": "继续",
|
||||
},
|
||||
Language.EN: {
|
||||
# CLI welcome and general
|
||||
"cli_welcome": "Welcome to AstrBot CLI!",
|
||||
"cli_version": "AstrBot CLI version: {version}",
|
||||
"cli_unknown_command": "Unknown command: {command}",
|
||||
"cli_help_available": "Use astrbot help --all to see all commands",
|
||||
|
||||
# Dashboard commands
|
||||
"dashboard_bundled": "Dashboard is bundled with the package - skipping download",
|
||||
"dashboard_not_installed": "Dashboard is not installed",
|
||||
"dashboard_install_confirm": "Install Dashboard?",
|
||||
"dashboard_installing": "Installing Dashboard...",
|
||||
"dashboard_install_success": "Dashboard installed successfully",
|
||||
"dashboard_install_failed": "Failed to install dashboard: {error}",
|
||||
"dashboard_not_needed": "Dashboard not needed",
|
||||
"dashboard_declined": "Dashboard installation declined.",
|
||||
"dashboard_already_up_to_date": "Dashboard is already up to date",
|
||||
"dashboard_version": "Dashboard version: {version}",
|
||||
"dashboard_download_failed": "Failed to download dashboard: {error}",
|
||||
"dashboard_init_dir": "Initializing dashboard directory...",
|
||||
"dashboard_init_success": "Dashboard initialized successfully",
|
||||
|
||||
# Plugin commands
|
||||
"plugin_installing": "Installing plugin: {name}",
|
||||
"plugin_install_success": "Plugin installed successfully: {name}",
|
||||
"plugin_install_failed": "Failed to install plugin: {name}",
|
||||
"plugin_uninstall_confirm": "Uninstall plugin {name}?",
|
||||
"plugin_uninstall_success": "Plugin uninstalled successfully: {name}",
|
||||
"plugin_uninstall_failed": "Failed to uninstall plugin: {name}",
|
||||
"plugin_list_empty": "No plugins installed",
|
||||
"plugin_already_installed": "Plugin already installed: {name}",
|
||||
"plugin_not_found": "Plugin not found: {name}",
|
||||
"plugin_already_exists": "Plugin {name} already exists",
|
||||
"plugin_not_found_or_installed": "Plugin {name} not found or already installed",
|
||||
"plugin_uninstall_failed_ex": "Failed to uninstall plugin {name}: {error}",
|
||||
"plugin_no_update_needed": "No plugins need updating",
|
||||
"plugin_found_update": "Found {count} plugin(s) needing update",
|
||||
"plugin_updating": "Updating plugin {name}...",
|
||||
"plugin_search_no_result": "No plugins matching '{query}' found",
|
||||
"plugin_search_results": "Search results: '{query}'",
|
||||
|
||||
# Config commands
|
||||
"config_show": "Show configuration",
|
||||
"config_set_success": "Configuration updated: {key} = {value}",
|
||||
"config_set_failed": "Failed to update configuration: {key}",
|
||||
"config_set_failed_ex": "Failed to set config: {error}",
|
||||
"config_get_success": "{key} = {value}",
|
||||
"config_get_not_found": "Configuration key not found: {key}",
|
||||
"config_reset_confirm": "Reset all configuration?",
|
||||
"config_reset_success": "Configuration reset",
|
||||
|
||||
# Config validators
|
||||
"config_log_level_invalid": "Log level must be one of DEBUG/INFO/WARNING/ERROR/CRITICAL",
|
||||
"config_port_must_be_number": "Port must be a number",
|
||||
"config_port_range_invalid": "Port must be in range 1-65535",
|
||||
"config_username_empty": "Username cannot be empty",
|
||||
"config_password_empty": "Password cannot be empty",
|
||||
"config_timezone_invalid": "Invalid timezone: {value}. Please use a valid IANA timezone name",
|
||||
"config_callback_invalid": "Callback API base must start with http:// or https://",
|
||||
"config_key_unsupported": "Unsupported config key: {key}",
|
||||
"config_key_unknown": "Unknown config key: {key}",
|
||||
"config_updated": "Config updated: {key}",
|
||||
|
||||
# Init command
|
||||
"init_creating": "Creating config directory...",
|
||||
"init_created": "Config directory created: {path}",
|
||||
"init_copying": "Copying config files...",
|
||||
"init_copied": "Config files copied",
|
||||
"init_success": "AstrBot initialized successfully!",
|
||||
"init_failed": "Initialization failed: {error}",
|
||||
|
||||
# Run command
|
||||
"run_starting": "Starting AstrBot...",
|
||||
"run_started": "AstrBot started!",
|
||||
"run_backend_only": "Starting in backend-only mode",
|
||||
"run_failed": "Failed to start: {error}",
|
||||
"run_stopped": "AstrBot stopped",
|
||||
|
||||
# TUI command
|
||||
"tui_starting": "Starting TUI...",
|
||||
"tui_started": "TUI started",
|
||||
"tui_failed": "Failed to start TUI: {error}",
|
||||
|
||||
# Common
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"loading": "Loading...",
|
||||
"done": "Done",
|
||||
"failed": "Failed",
|
||||
"retry": "Retry",
|
||||
"exit": "Exit",
|
||||
"continue": "Continue",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_current_language() -> Language:
|
||||
"""Get the current language based on environment or default.
|
||||
|
||||
Detection order:
|
||||
1. ASTRBOT_CLI_LANG environment variable (zh/en)
|
||||
2. LANG environment variable (if contains zh/cn)
|
||||
3. LC_ALL environment variable (if contains zh/cn)
|
||||
4. Default to Chinese (most users are Chinese)
|
||||
"""
|
||||
# Check explicit override first
|
||||
explicit = os.environ.get("ASTRBOT_CLI_LANG", "").lower()
|
||||
if explicit in ("zh", "en"):
|
||||
return Language.ZH if explicit == "zh" else Language.EN
|
||||
|
||||
# Check LANG/LC_ALL for Chinese
|
||||
for env_var in ("LANG", "LC_ALL"):
|
||||
lang = os.environ.get(env_var, "").lower()
|
||||
if "zh" in lang or "cn" in lang:
|
||||
return Language.ZH
|
||||
|
||||
# Default to Chinese for broader appeal
|
||||
return Language.ZH
|
||||
|
||||
|
||||
def set_language(lang: Language) -> None:
|
||||
"""Set the current language (clears all translation caches)."""
|
||||
get_current_language.cache_clear()
|
||||
_t_cached.cache_clear()
|
||||
# Set environment variable for persistence
|
||||
os.environ["ASTRBOT_CLI_LANG"] = lang.value
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _t_cached(key: str, lang: Language) -> str:
|
||||
"""Cached translation lookup."""
|
||||
return _TRANSLATIONS.get(lang, {}).get(key, key)
|
||||
|
||||
|
||||
def t(translation_key: str, **kwargs: str) -> str:
|
||||
"""Get translation for the given key in the current language.
|
||||
|
||||
Args:
|
||||
translation_key: Translation key (e.g., "cli_welcome", "plugin_installing")
|
||||
**kwargs: Format arguments for the translation string
|
||||
|
||||
Returns:
|
||||
Translated string, or the key itself if not found
|
||||
"""
|
||||
result = _t_cached(translation_key, get_current_language())
|
||||
if kwargs:
|
||||
result = result.format(**kwargs)
|
||||
return result
|
||||
|
||||
|
||||
def tr(key: str, **kwargs: str) -> str:
|
||||
"""Get translation (alias for t())."""
|
||||
return t(key, **kwargs)
|
||||
|
||||
|
||||
class CLITranslations:
|
||||
"""Translation accessor class for CLI contexts.
|
||||
|
||||
Usage:
|
||||
translations = CLITranslations()
|
||||
print(translations.cli_welcome)
|
||||
print(translations.plugin_installing(name="my_plugin"))
|
||||
"""
|
||||
|
||||
def __getattr__(self, key: str) -> str:
|
||||
return t(key)
|
||||
|
||||
def __call__(self, key: str, **kwargs: str) -> str:
|
||||
return t(key, **kwargs)
|
||||
|
||||
|
||||
# Convenience instance
|
||||
translations = CLITranslations()
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from astrbot.cli.i18n import t
|
||||
|
||||
from .version_comparator import VersionComparator
|
||||
|
||||
|
||||
@@ -17,25 +18,20 @@ class DashboardManager:
|
||||
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
|
||||
|
||||
if self._bundled_dist.is_dir():
|
||||
click.echo("Dashboard is bundled with the package - skipping download.")
|
||||
click.echo(t("dashboard_bundled"))
|
||||
return
|
||||
|
||||
try:
|
||||
dashboard_version = await get_dashboard_version()
|
||||
match dashboard_version:
|
||||
case None:
|
||||
click.echo("Dashboard is not installed")
|
||||
click.echo(t("dashboard_not_installed"))
|
||||
# Skip interactive prompt in non-interactive environments
|
||||
if not sys.stdin.isatty():
|
||||
click.echo(
|
||||
"Skipping interactive dashboard installation in non-interactive mode."
|
||||
)
|
||||
click.echo(t("dashboard_not_needed"))
|
||||
return
|
||||
if click.confirm(
|
||||
"Install dashboard?",
|
||||
default=True,
|
||||
):
|
||||
click.echo("Installing dashboard...")
|
||||
if click.confirm(t("dashboard_install_confirm"), default=True):
|
||||
click.echo(t("dashboard_installing"))
|
||||
try:
|
||||
await download_dashboard(
|
||||
path="data/dashboard.zip",
|
||||
@@ -43,22 +39,22 @@ class DashboardManager:
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
)
|
||||
click.echo("Dashboard installed successfully")
|
||||
click.echo(t("dashboard_install_success"))
|
||||
except Exception as e:
|
||||
click.echo(f"Failed to install dashboard: {e}")
|
||||
click.echo(t("dashboard_install_failed", error=str(e)))
|
||||
else:
|
||||
click.echo("Dashboard installation declined.")
|
||||
click.echo(t("dashboard_declined"))
|
||||
|
||||
case str():
|
||||
if (
|
||||
VersionComparator.compare_version(VERSION, dashboard_version)
|
||||
<= 0
|
||||
):
|
||||
click.echo("Dashboard is already up to date")
|
||||
click.echo(t("dashboard_already_up_to_date"))
|
||||
return
|
||||
try:
|
||||
version = dashboard_version.split("v")[1]
|
||||
click.echo(f"Dashboard version: {version}")
|
||||
click.echo(t("dashboard_version", version=version))
|
||||
await download_dashboard(
|
||||
path="data/dashboard.zip",
|
||||
extract_path=str(astrbot_root / "data"),
|
||||
@@ -66,10 +62,10 @@ class DashboardManager:
|
||||
latest=False,
|
||||
)
|
||||
except Exception as e:
|
||||
click.echo(f"Failed to download dashboard: {e}")
|
||||
click.echo(t("dashboard_download_failed", error=str(e)))
|
||||
return
|
||||
except FileNotFoundError:
|
||||
click.echo("Initializing dashboard directory...")
|
||||
click.echo(t("dashboard_init_dir"))
|
||||
try:
|
||||
await download_dashboard(
|
||||
path=str(astrbot_root / "data" / "dashboard.zip"),
|
||||
@@ -77,7 +73,7 @@ class DashboardManager:
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
)
|
||||
click.echo("Dashboard initialized successfully")
|
||||
click.echo(t("dashboard_init_success"))
|
||||
except Exception as e:
|
||||
click.echo(f"Failed to download dashboard: {e}")
|
||||
click.echo(t("dashboard_download_failed", error=str(e)))
|
||||
return
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
"""
|
||||
MCP client - DEPRECATED
|
||||
|
||||
.. deprecated::
|
||||
This module has been moved to :mod:`astrbot._internal.mcp`.
|
||||
Please update your imports accordingly.
|
||||
|
||||
Old import (deprecated):
|
||||
from astrbot.core.agent.mcp_client import MCPClient, MCPTool
|
||||
|
||||
New import:
|
||||
from astrbot._internal.mcp import MCPClient, MCPTool
|
||||
|
||||
This file exists solely for backward compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"astrbot.core.agent.mcp_client has been moved to astrbot._internal.mcp. "
|
||||
"Please update your imports.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
from .skill_manager import SkillInfo, SkillManager, build_skills_prompt
|
||||
"""
|
||||
AstrBot skills module - DEPRECATED
|
||||
|
||||
__all__ = ["SkillInfo", "SkillManager", "build_skills_prompt"]
|
||||
.. deprecated::
|
||||
This module has been moved to :mod:`astrbot._internal.skills`.
|
||||
Please update your imports accordingly.
|
||||
|
||||
Old import (deprecated):
|
||||
from astrbot.core.skills import SkillManager, SkillInfo
|
||||
|
||||
New import:
|
||||
from astrbot._internal.skills import SkillManager, SkillInfo
|
||||
|
||||
This file exists solely for backward compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"astrbot.core.skills has been moved to astrbot._internal.skills. "
|
||||
"Please update your imports.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# Re-export from new location for backward compatibility
|
||||
from astrbot._internal.skills import (
|
||||
SkillInfo,
|
||||
SkillManager,
|
||||
build_skills_prompt,
|
||||
SkillToToolConverter,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"SkillInfo",
|
||||
"SkillManager",
|
||||
"build_skills_prompt",
|
||||
"SkillToToolConverter",
|
||||
]
|
||||
|
||||
47
astrbot/core/tools/__init__.py
Normal file
47
astrbot/core/tools/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
AstrBot core tools - DEPRECATED
|
||||
|
||||
.. deprecated::
|
||||
This module has been moved to :mod:`astrbot._internal.tools.builtin`.
|
||||
Please update your imports accordingly.
|
||||
|
||||
Old import (deprecated):
|
||||
from astrbot.core.tools import cron_tools, send_message, kb_query
|
||||
|
||||
New import:
|
||||
from astrbot._internal.tools.builtin import (
|
||||
CreateActiveCronTool,
|
||||
DeleteCronJobTool,
|
||||
ListCronJobsTool,
|
||||
SendMessageToUserTool,
|
||||
KnowledgeBaseQueryTool,
|
||||
)
|
||||
|
||||
This file exists solely for backward compatibility and will be removed in a future version.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"astrbot.core.tools has been moved to astrbot._internal.tools.builtin. "
|
||||
"Please update your imports.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# Re-export from new location for backward compatibility
|
||||
from astrbot._internal.tools.builtin import (
|
||||
CREATE_CRON_JOB_TOOL,
|
||||
DELETE_CRON_JOB_TOOL,
|
||||
KNOWLEDGE_BASE_QUERY_TOOL,
|
||||
LIST_CRON_JOBS_TOOL,
|
||||
SEND_MESSAGE_TO_USER_TOOL,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CREATE_CRON_JOB_TOOL",
|
||||
"DELETE_CRON_JOB_TOOL",
|
||||
"KNOWLEDGE_BASE_QUERY_TOOL",
|
||||
"LIST_CRON_JOBS_TOOL",
|
||||
"SEND_MESSAGE_TO_USER_TOOL",
|
||||
]
|
||||
183
astrbot/tui/i18n.py
Normal file
183
astrbot/tui/i18n.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""Internationalization support for AstrBot TUI.
|
||||
|
||||
This module provides i18n support with Chinese and English languages.
|
||||
Language is auto-detected from environment or can be set manually.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Language(Enum):
|
||||
"""Supported languages."""
|
||||
|
||||
ZH = "zh"
|
||||
EN = "en"
|
||||
|
||||
|
||||
# Translation dictionaries
|
||||
_TRANSLATIONS: dict[Language, dict[str, str]] = {
|
||||
Language.ZH: {
|
||||
# Welcome messages
|
||||
"welcome_title": "欢迎使用 AstrBot TUI",
|
||||
"welcome_local_mode": "本地测试模式",
|
||||
"welcome_instructions": "输入消息后按 Enter 发送, ESC 或 Ctrl+C 退出",
|
||||
"welcome_language": "语言已自动检测为中文",
|
||||
|
||||
# Status messages
|
||||
"status_ready": "就绪",
|
||||
"status_connected": "已连接",
|
||||
"status_disconnected": "未连接",
|
||||
"status_processing": "处理中...",
|
||||
"status_sending": "发送中...",
|
||||
|
||||
# Message indicators
|
||||
"indicator_user": "我",
|
||||
"indicator_bot": "AI",
|
||||
"indicator_system": "系统",
|
||||
"indicator_tool": "工具",
|
||||
"indicator_reasoning": "推理",
|
||||
|
||||
# Input hints
|
||||
"input_prompt": "> ",
|
||||
"input_placeholder": "输入消息...",
|
||||
|
||||
# Error messages
|
||||
"error_empty_message": "消息不能为空",
|
||||
"error_send_failed": "发送失败",
|
||||
"error_connection_lost": "连接已断开",
|
||||
"error_unknown": "未知错误",
|
||||
|
||||
# Tool messages
|
||||
"tool_using": "使用工具中",
|
||||
"tool_completed": "工具执行完成",
|
||||
"tool_failed": "工具执行失败",
|
||||
|
||||
# Reasoning messages
|
||||
"reasoning_thinking": "思考中...",
|
||||
"reasoning_reasoning": "推理中...",
|
||||
},
|
||||
Language.EN: {
|
||||
# Welcome messages
|
||||
"welcome_title": "Welcome to AstrBot TUI",
|
||||
"welcome_local_mode": "Local Testing Mode",
|
||||
"welcome_instructions": "Type your message and press Enter to send. ESC or Ctrl+C to exit.",
|
||||
"welcome_language": "Language auto-detected as English",
|
||||
|
||||
# Status messages
|
||||
"status_ready": "Ready",
|
||||
"status_connected": "Connected",
|
||||
"status_disconnected": "Disconnected",
|
||||
"status_processing": "Processing...",
|
||||
"status_sending": "Sending...",
|
||||
|
||||
# Message indicators
|
||||
"indicator_user": "Me",
|
||||
"indicator_bot": "AI",
|
||||
"indicator_system": "Sys",
|
||||
"indicator_tool": "Tool",
|
||||
"indicator_reasoning": "Reason",
|
||||
|
||||
# Input hints
|
||||
"input_prompt": "> ",
|
||||
"input_placeholder": "Type a message...",
|
||||
|
||||
# Error messages
|
||||
"error_empty_message": "Message cannot be empty",
|
||||
"error_send_failed": "Failed to send",
|
||||
"error_connection_lost": "Connection lost",
|
||||
"error_unknown": "Unknown error",
|
||||
|
||||
# Tool messages
|
||||
"tool_using": "Using tool",
|
||||
"tool_completed": "Tool completed",
|
||||
"tool_failed": "Tool failed",
|
||||
|
||||
# Reasoning messages
|
||||
"reasoning_thinking": "Thinking...",
|
||||
"reasoning_reasoning": "Reasoning...",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_current_language() -> Language:
|
||||
"""Get the current language based on environment or default.
|
||||
|
||||
Detection order:
|
||||
1. ASTRBOT_TUI_LANG environment variable (zh/en)
|
||||
2. LANG environment variable (if contains zh/cn)
|
||||
3. LC_ALL environment variable (if contains zh/cn)
|
||||
4. Default to Chinese (most users are Chinese)
|
||||
"""
|
||||
# Check explicit override first
|
||||
explicit = os.environ.get("ASTRBOT_TUI_LANG", "").lower()
|
||||
if explicit in ("zh", "en"):
|
||||
return Language.ZH if explicit == "zh" else Language.EN
|
||||
|
||||
# Check LANG/LC_ALL for Chinese
|
||||
for env_var in ("LANG", "LC_ALL"):
|
||||
lang = os.environ.get(env_var, "").lower()
|
||||
if "zh" in lang or "cn" in lang:
|
||||
return Language.ZH
|
||||
|
||||
# Default to Chinese for broader appeal
|
||||
return Language.ZH
|
||||
|
||||
|
||||
def set_language(lang: Language) -> None:
|
||||
"""Set the current language (clears all translation caches)."""
|
||||
get_current_language.cache_clear()
|
||||
_t_cached.cache_clear()
|
||||
# Set environment variable for persistence
|
||||
os.environ["ASTRBOT_TUI_LANG"] = lang.value
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _t_cached(translation_key: str, lang: Language) -> str:
|
||||
"""Cached translation lookup."""
|
||||
return _TRANSLATIONS.get(lang, {}).get(translation_key, translation_key)
|
||||
|
||||
|
||||
def t(translation_key: str) -> str:
|
||||
"""Get translation for the given key in the current language.
|
||||
|
||||
Args:
|
||||
translation_key: Translation key (e.g., "welcome_title", "status_ready")
|
||||
|
||||
Returns:
|
||||
Translated string, or the key itself if not found
|
||||
"""
|
||||
return _t_cached(translation_key, get_current_language())
|
||||
|
||||
|
||||
def tr(translation_key: str) -> str:
|
||||
"""Get translation (alias for t())."""
|
||||
return t(translation_key)
|
||||
|
||||
|
||||
class TUITranslations:
|
||||
"""Translation accessor class for non-function contexts.
|
||||
|
||||
Usage:
|
||||
translations = TUITranslations()
|
||||
print(translations.WELCOME_TITLE)
|
||||
"""
|
||||
|
||||
def __getattr__(self, key: str) -> str:
|
||||
return t(key)
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return t(key)
|
||||
|
||||
def get(self, key: str, default: str | None = None) -> str:
|
||||
"""Get translation with default."""
|
||||
result = t(key)
|
||||
return default if result == key and default else result
|
||||
|
||||
|
||||
# Convenience instance
|
||||
translations = TUITranslations()
|
||||
@@ -6,6 +6,8 @@ import curses
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
|
||||
from astrbot.tui.i18n import t
|
||||
|
||||
|
||||
class ColorPair(Enum):
|
||||
WHITE = 1
|
||||
@@ -156,7 +158,7 @@ class Screen:
|
||||
if not self._header_win:
|
||||
return
|
||||
self._header_win.clear()
|
||||
title = " AstrBot TUI "
|
||||
title = f" {t('welcome_title')} "
|
||||
|
||||
try:
|
||||
self._header_win.bkgdset(curses.color_pair(ColorPair.HEADER_FG.value))
|
||||
@@ -201,20 +203,25 @@ class Screen:
|
||||
if y >= max_y:
|
||||
break
|
||||
|
||||
# Get localized indicator
|
||||
indicator_map = {
|
||||
"user": t("indicator_user"),
|
||||
"bot": t("indicator_bot"),
|
||||
"tool": t("indicator_tool"),
|
||||
"reasoning": t("indicator_reasoning"),
|
||||
"system": t("indicator_system"),
|
||||
}
|
||||
indicator = indicator_map.get(sender, t("indicator_system"))
|
||||
|
||||
if sender == "user":
|
||||
indicator = ">"
|
||||
color = self.get_color(ColorPair.USER_MSG)
|
||||
elif sender == "bot":
|
||||
indicator = "✦"
|
||||
color = self.get_color(ColorPair.BOT_MSG)
|
||||
elif sender == "tool":
|
||||
indicator = "⚙"
|
||||
color = self.get_color(ColorPair.TOOL_MSG)
|
||||
elif sender == "reasoning":
|
||||
indicator = "◎"
|
||||
color = self.get_color(ColorPair.REASONING_MSG)
|
||||
else:
|
||||
indicator = "●"
|
||||
color = self.get_color(ColorPair.SYSTEM_MSG)
|
||||
|
||||
max_text_width = self.width - 4
|
||||
@@ -260,8 +267,8 @@ class Screen:
|
||||
return
|
||||
self._input_win.clear()
|
||||
|
||||
prompt = "> "
|
||||
prompt_len = 2
|
||||
prompt = t("input_prompt")
|
||||
prompt_len = len(prompt)
|
||||
max_input_width = self.width - 2
|
||||
|
||||
try:
|
||||
|
||||
@@ -10,6 +10,7 @@ import curses
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
from astrbot.tui.i18n import TUITranslations, t
|
||||
from astrbot.tui.screen import Screen
|
||||
|
||||
|
||||
@@ -21,6 +22,25 @@ class MessageSender(Enum):
|
||||
REASONING = "reasoning"
|
||||
|
||||
|
||||
# Translation accessor for templates
|
||||
tr = TUITranslations()
|
||||
|
||||
|
||||
# Mapping from sender to translation key
|
||||
_SENDER_TO_KEY = {
|
||||
MessageSender.USER: "indicator_user",
|
||||
MessageSender.BOT: "indicator_bot",
|
||||
MessageSender.SYSTEM: "indicator_system",
|
||||
MessageSender.TOOL: "indicator_tool",
|
||||
MessageSender.REASONING: "indicator_reasoning",
|
||||
}
|
||||
|
||||
|
||||
def get_indicator(sender: MessageSender) -> str:
|
||||
"""Get the localized indicator string for a message sender."""
|
||||
return t(_SENDER_TO_KEY.get(sender, "indicator_system"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
sender: MessageSender
|
||||
@@ -33,7 +53,7 @@ class TUIState:
|
||||
messages: list[Message] = field(default_factory=list)
|
||||
input_buffer: str = ""
|
||||
cursor_x: int = 0
|
||||
status: str = "Ready"
|
||||
status: str = field(default_factory=lambda: t("status_ready"))
|
||||
running: bool = True
|
||||
connected: bool = False
|
||||
|
||||
@@ -182,9 +202,9 @@ class AstrBotTUI:
|
||||
self.screen.layout_windows()
|
||||
|
||||
# Welcome message
|
||||
self.add_system_message("Welcome to AstrBot TUI (local mode)!")
|
||||
self.add_system_message("Type your message and press Enter to send.")
|
||||
self.add_system_message("Press ESC or Ctrl+C to exit.")
|
||||
self.add_system_message(t("welcome_title"))
|
||||
self.add_system_message(t("welcome_local_mode"))
|
||||
self.add_system_message(t("welcome_instructions"))
|
||||
|
||||
# Initial render
|
||||
self.render()
|
||||
|
||||
Reference in New Issue
Block a user