mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-01 18:20:16 +08:00
Compare commits
1 Commits
codex/fix-
...
feat/runti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ab472f07 |
@@ -3,6 +3,8 @@ from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.utils.io import download_dashboard
|
||||
|
||||
from ..i18n import t
|
||||
|
||||
|
||||
class AdminCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
@@ -10,6 +12,6 @@ class AdminCommands:
|
||||
|
||||
async def update_dashboard(self, event: AstrMessageEvent) -> None:
|
||||
"""更新管理面板"""
|
||||
await event.send(MessageChain().message("⏳ Updating dashboard..."))
|
||||
await event.send(MessageChain().message(t(self.context, "dashboard.updating")))
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
await event.send(MessageChain().message("✅ Dashboard updated successfully."))
|
||||
await event.send(MessageChain().message(t(self.context, "dashboard.updated")))
|
||||
|
||||
@@ -9,6 +9,7 @@ from astrbot.core.agent.runners.deerflow.constants import (
|
||||
from astrbot.core.agent.runners.deerflow.deerflow_api_client import DeerFlowAPIClient
|
||||
from astrbot.core.utils.active_event_registry import active_event_registry
|
||||
|
||||
from ..i18n import t
|
||||
from .utils.rst_scene import RstScene
|
||||
|
||||
THIRD_PARTY_AGENT_RUNNER_KEY = {
|
||||
@@ -138,8 +139,12 @@ class ConversationCommands:
|
||||
if required_perm == "admin" and message.role != "admin":
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"Reset command requires admin permission in {scene.name} scenario, "
|
||||
f"you (ID {message.get_sender_id()}) are not admin, cannot perform this action.",
|
||||
t(
|
||||
self.context,
|
||||
"conversation.reset_admin_required",
|
||||
scene_name=t(self.context, f"scene.{scene.key}"),
|
||||
sender_id=message.get_sender_id(),
|
||||
),
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -153,14 +158,16 @@ class ConversationCommands:
|
||||
agent_runner_type,
|
||||
)
|
||||
message.set_result(
|
||||
MessageEventResult().message("✅ Conversation reset successfully.")
|
||||
MessageEventResult().message(
|
||||
t(self.context, "conversation.reset_success"),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not self.context.get_using_provider(umo):
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"😕 Cannot find any LLM provider. Configure one first."
|
||||
t(self.context, "conversation.no_provider"),
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -170,7 +177,7 @@ class ConversationCommands:
|
||||
if not cid:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"😕 You are not in a conversation. Use /new to create one.",
|
||||
t(self.context, "conversation.no_conversation"),
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -183,7 +190,7 @@ class ConversationCommands:
|
||||
[],
|
||||
)
|
||||
|
||||
ret = "✅ Conversation reset successfully."
|
||||
ret = t(self.context, "conversation.reset_success")
|
||||
|
||||
message.set_extra("_clean_ltm_session", True)
|
||||
|
||||
@@ -206,13 +213,19 @@ class ConversationCommands:
|
||||
if stopped_count > 0:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"✅ Requested to stop {stopped_count} running tasks."
|
||||
t(
|
||||
self.context,
|
||||
"conversation.stop_requested",
|
||||
count=stopped_count,
|
||||
),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
message.set_result(
|
||||
MessageEventResult().message("✅ No running tasks in the current session.")
|
||||
MessageEventResult().message(
|
||||
t(self.context, "conversation.no_running_tasks"),
|
||||
)
|
||||
)
|
||||
|
||||
async def new_conv(self, message: AstrMessageEvent) -> None:
|
||||
@@ -227,7 +240,9 @@ class ConversationCommands:
|
||||
agent_runner_type,
|
||||
)
|
||||
message.set_result(
|
||||
MessageEventResult().message("✅ New conversation created.")
|
||||
MessageEventResult().message(
|
||||
t(self.context, "conversation.new_created")
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -243,6 +258,10 @@ class ConversationCommands:
|
||||
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"✅ Switched to new conversation: {cid[:4]}。"
|
||||
t(
|
||||
self.context,
|
||||
"conversation.switched_new",
|
||||
conversation_id=cid[:4],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,6 +6,8 @@ from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.star import command_management
|
||||
from astrbot.core.utils.io import get_dashboard_version
|
||||
|
||||
from ..i18n import t
|
||||
|
||||
|
||||
class HelpCommand:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
@@ -77,7 +79,7 @@ class HelpCommand:
|
||||
commands_section = (
|
||||
"\n".join(command_lines)
|
||||
if command_lines
|
||||
else "No enabled built-in commands."
|
||||
else t(self.context, "help.no_enabled_builtin_commands")
|
||||
)
|
||||
|
||||
msg_parts = [
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from astrbot.api import sp, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
|
||||
from ..i18n import t
|
||||
|
||||
|
||||
class SetUnsetCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
@@ -15,7 +17,12 @@ class SetUnsetCommands:
|
||||
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
f"会话 {uid} 变量 {key} 存储成功。使用 /unset 移除。",
|
||||
t(
|
||||
self.context,
|
||||
"setunset.set_success",
|
||||
session_id=uid,
|
||||
key=key,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -26,11 +33,20 @@ class SetUnsetCommands:
|
||||
|
||||
if key not in session_var:
|
||||
event.set_result(
|
||||
MessageEventResult().message("没有那个变量名。格式 /unset 变量名。"),
|
||||
MessageEventResult().message(
|
||||
t(self.context, "setunset.unset_not_found")
|
||||
),
|
||||
)
|
||||
else:
|
||||
del session_var[key]
|
||||
await sp.session_put(uid, "session_variables", session_var)
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"会话 {uid} 变量 {key} 移除成功。"),
|
||||
MessageEventResult().message(
|
||||
t(
|
||||
self.context,
|
||||
"setunset.unset_success",
|
||||
session_id=uid,
|
||||
key=key,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from astrbot.api import star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
|
||||
from ..i18n import t
|
||||
|
||||
|
||||
class SIDCommand:
|
||||
"""会话ID命令类"""
|
||||
@@ -17,20 +19,24 @@ class SIDCommand:
|
||||
umo_platform = event.session.platform_id
|
||||
umo_msg_type = event.session.message_type.value
|
||||
umo_session_id = event.session.session_id
|
||||
ret = (
|
||||
f"UMO: 「{sid}」\n"
|
||||
f"UID: 「{user_id}」\n"
|
||||
"*Use UMO to set whitelist and configure routing, use UID to set admin list(UMO 可用于设置白名单和配置文件路由,UID 可用于设置管理员列表)\n\n"
|
||||
f"Your session information:\n"
|
||||
f"Bot ID: 「{umo_platform}」\n"
|
||||
f"Message Type: 「{umo_msg_type}」\n"
|
||||
f"Session ID: 「{umo_session_id}」\n\n"
|
||||
ret = t(
|
||||
self.context,
|
||||
"sid.info",
|
||||
sid=sid,
|
||||
user_id=user_id,
|
||||
platform=umo_platform,
|
||||
message_type=umo_msg_type,
|
||||
session_id=umo_session_id,
|
||||
)
|
||||
|
||||
if (
|
||||
self.context.get_config()["platform_settings"]["unique_session"]
|
||||
and event.get_group_id()
|
||||
):
|
||||
ret += f"\n\nThe group's ID: 「{event.get_group_id()}」. Set this ID to whitelist to allow the entire group."
|
||||
ret += t(
|
||||
self.context,
|
||||
"sid.group_whitelist",
|
||||
group_id=event.get_group_id(),
|
||||
)
|
||||
|
||||
event.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
|
||||
30
astrbot/builtin_stars/builtin_commands/i18n.py
Normal file
30
astrbot/builtin_stars/builtin_commands/i18n.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
LOCALE_DIR = Path(__file__).resolve().parent / "locales"
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _load_locale(language: str) -> dict[str, Any]:
|
||||
with (LOCALE_DIR / f"{language}.json").open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _resolve_key(data: dict[str, Any], translation_key: str) -> Any:
|
||||
value: Any = data
|
||||
for part in translation_key.split("."):
|
||||
if not isinstance(value, dict) or part not in value:
|
||||
return None
|
||||
value = value[part]
|
||||
return value
|
||||
|
||||
|
||||
def t(context: Any, translation_key: str, **kwargs: Any) -> str:
|
||||
text = _resolve_key(_load_locale(context.get_current_language()), translation_key)
|
||||
if not isinstance(text, str):
|
||||
return translation_key
|
||||
if not kwargs:
|
||||
return text
|
||||
return text.format(**kwargs)
|
||||
33
astrbot/builtin_stars/builtin_commands/locales/en-US.json
Normal file
33
astrbot/builtin_stars/builtin_commands/locales/en-US.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"help": {
|
||||
"no_enabled_builtin_commands": "No enabled built-in commands."
|
||||
},
|
||||
"sid": {
|
||||
"info": "UMO: 「{sid}」\nUID: 「{user_id}」\n*Use UMO to set whitelist and configure routing, use UID to set admin list.\n\nYour session information:\nBot ID: 「{platform}」\nMessage Type: 「{message_type}」\nSession ID: 「{session_id}」\n\n",
|
||||
"group_whitelist": "\n\nThe group's ID: 「{group_id}」. Set this ID to whitelist to allow the entire group."
|
||||
},
|
||||
"dashboard": {
|
||||
"updating": "⏳ Updating dashboard...",
|
||||
"updated": "✅ Dashboard updated successfully."
|
||||
},
|
||||
"scene": {
|
||||
"group_unique_on": "group chat with unique session enabled",
|
||||
"group_unique_off": "group chat with unique session disabled",
|
||||
"private": "private chat"
|
||||
},
|
||||
"conversation": {
|
||||
"reset_admin_required": "Reset command requires admin permission in {scene_name} scenario, you (ID {sender_id}) are not admin, cannot perform this action.",
|
||||
"reset_success": "✅ Conversation reset successfully.",
|
||||
"no_provider": "😕 Cannot find any LLM provider. Configure one first.",
|
||||
"no_conversation": "😕 You are not in a conversation. Use /new to create one.",
|
||||
"stop_requested": "✅ Requested to stop {count} running tasks.",
|
||||
"no_running_tasks": "✅ No running tasks in the current session.",
|
||||
"new_created": "✅ New conversation created.",
|
||||
"switched_new": "✅ Switched to new conversation: {conversation_id}."
|
||||
},
|
||||
"setunset": {
|
||||
"set_success": "Session {session_id} variable {key} saved. Use /unset to remove it.",
|
||||
"unset_not_found": "No variable with that name. Format: /unset variable_name.",
|
||||
"unset_success": "Session {session_id} variable {key} removed."
|
||||
}
|
||||
}
|
||||
33
astrbot/builtin_stars/builtin_commands/locales/zh-CN.json
Normal file
33
astrbot/builtin_stars/builtin_commands/locales/zh-CN.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"help": {
|
||||
"no_enabled_builtin_commands": "没有已启用的内置指令。"
|
||||
},
|
||||
"sid": {
|
||||
"info": "UMO: 「{sid}」\nUID: 「{user_id}」\n*使用 UMO 设置白名单和配置文件路由,使用 UID 设置管理员列表。\n\n当前会话信息:\n机器人 ID: 「{platform}」\n消息类型: 「{message_type}」\n会话 ID: 「{session_id}」\n\n",
|
||||
"group_whitelist": "\n\n当前群聊 ID: 「{group_id}」。将此 ID 加入白名单可允许整个群聊。"
|
||||
},
|
||||
"dashboard": {
|
||||
"updating": "⏳ 正在更新管理面板...",
|
||||
"updated": "✅ 管理面板更新成功。"
|
||||
},
|
||||
"scene": {
|
||||
"group_unique_on": "群聊+会话隔离开启",
|
||||
"group_unique_off": "群聊+会话隔离关闭",
|
||||
"private": "私聊"
|
||||
},
|
||||
"conversation": {
|
||||
"reset_admin_required": "在 {scene_name} 场景下,reset 指令需要管理员权限。你(ID {sender_id})不是管理员,无法执行此操作。",
|
||||
"reset_success": "✅ 会话已重置。",
|
||||
"no_provider": "😕 未找到可用的 LLM 提供商,请先配置。",
|
||||
"no_conversation": "😕 当前未处于对话状态。请使用 /new 创建一个对话。",
|
||||
"stop_requested": "✅ 已请求停止 {count} 个正在运行的任务。",
|
||||
"no_running_tasks": "✅ 当前会话没有正在运行的任务。",
|
||||
"new_created": "✅ 已创建新对话。",
|
||||
"switched_new": "✅ 已切换到新对话:{conversation_id}。"
|
||||
},
|
||||
"setunset": {
|
||||
"set_success": "会话 {session_id} 变量 {key} 存储成功。使用 /unset 移除。",
|
||||
"unset_not_found": "没有那个变量名。格式 /unset 变量名。",
|
||||
"unset_success": "会话 {session_id} 变量 {key} 移除成功。"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import os
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.i18n import Language
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.23.0"
|
||||
@@ -53,6 +54,7 @@ WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"config_version": 2,
|
||||
"language": Language.ZH_CN.value,
|
||||
"platform_settings": {
|
||||
"unique_session": False,
|
||||
"rate_limit": {
|
||||
@@ -4037,6 +4039,13 @@ CONFIG_METADATA_3_SYSTEM = {
|
||||
"description": "系统配置",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"language": {
|
||||
"description": "系统语言",
|
||||
"type": "string",
|
||||
"hint": "用于 AstrBot 运行时回复的语言。目前支持简体中文和英文。",
|
||||
"options": [Language.ZH_CN.value, Language.EN_US.value],
|
||||
"labels": ["简体中文", "English"],
|
||||
},
|
||||
"t2i_strategy": {
|
||||
"description": "文本转图像策略",
|
||||
"type": "string",
|
||||
|
||||
62
astrbot/core/i18n/__init__.py
Normal file
62
astrbot/core/i18n/__init__.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
CORE_LOCALE_DIR = Path(__file__).resolve().parent / "locales"
|
||||
|
||||
|
||||
class Language(str, Enum):
|
||||
ZH_CN = "zh-CN"
|
||||
EN_US = "en-US"
|
||||
|
||||
|
||||
DEFAULT_LANGUAGE = Language.ZH_CN.value
|
||||
|
||||
|
||||
def normalize_language(language: str | Language | None) -> str:
|
||||
if isinstance(language, Language):
|
||||
return language.value
|
||||
if language == Language.EN_US.value:
|
||||
return Language.EN_US.value
|
||||
return Language.ZH_CN.value
|
||||
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
def _load_locale(locale_dir: str, language: str) -> dict[str, Any]:
|
||||
locale_path = Path(locale_dir) / f"{language}.json"
|
||||
with locale_path.open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _resolve_key(data: dict[str, Any], key: str) -> Any:
|
||||
value: Any = data
|
||||
for part in key.split("."):
|
||||
if not isinstance(value, dict) or part not in value:
|
||||
return None
|
||||
value = value[part]
|
||||
return value
|
||||
|
||||
|
||||
def t(
|
||||
translation_key: str,
|
||||
*,
|
||||
locale: str | None = None,
|
||||
locale_dir: str | Path | None = None,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
language = normalize_language(locale)
|
||||
resolved_locale_dir = str(locale_dir or CORE_LOCALE_DIR)
|
||||
text = _resolve_key(_load_locale(resolved_locale_dir, language), translation_key)
|
||||
|
||||
if text is None and language != DEFAULT_LANGUAGE:
|
||||
text = _resolve_key(
|
||||
_load_locale(resolved_locale_dir, DEFAULT_LANGUAGE),
|
||||
translation_key,
|
||||
)
|
||||
if not isinstance(text, str):
|
||||
return translation_key
|
||||
if not kwargs:
|
||||
return text
|
||||
return text.format(**kwargs)
|
||||
12
astrbot/core/i18n/locales/en-US.json
Normal file
12
astrbot/core/i18n/locales/en-US.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"pipeline": {
|
||||
"filter_error": "Plugin {plugin_name}: {error}",
|
||||
"no_permission": "You (ID: {sender_id}) do not have permission to use this command. Use /sid to get your ID and ask an administrator to add it.",
|
||||
"content_blocked": "Your message or the model response contains inappropriate content and has been blocked.",
|
||||
"keyword_blocked_reason": "Content safety check failed because a sensitive keyword was matched.",
|
||||
"baidu_aip_violation_header": "Baidu content moderation found {count} violations:\n",
|
||||
"baidu_aip_conclusion": "\nConclusion: {conclusion}",
|
||||
"plugin_handler_error": ":(\n\nAn exception occurred while calling plugin {plugin_name}'s handler {handler_name}: {error}",
|
||||
"reasoning_prefix": "🤔 Thinking: {reasoning_content}\n"
|
||||
}
|
||||
}
|
||||
12
astrbot/core/i18n/locales/zh-CN.json
Normal file
12
astrbot/core/i18n/locales/zh-CN.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"pipeline": {
|
||||
"filter_error": "插件 {plugin_name}: {error}",
|
||||
"no_permission": "您(ID: {sender_id})的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。",
|
||||
"content_blocked": "你的消息或者大模型的响应中包含不适当的内容,已被屏蔽。",
|
||||
"keyword_blocked_reason": "内容安全检查不通过,匹配到敏感词。",
|
||||
"baidu_aip_violation_header": "百度审核服务发现 {count} 处违规:\n",
|
||||
"baidu_aip_conclusion": "\n判断结果:{conclusion}",
|
||||
"plugin_handler_error": ":(\n\n在调用插件 {plugin_name} 的处理函数 {handler_name} 时出现异常:{error}",
|
||||
"reasoning_prefix": "🤔 思考: {reasoning_content}\n"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.i18n import t
|
||||
from astrbot.core.message.message_event_result import MessageEventResult
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
|
||||
@@ -17,6 +18,7 @@ class ContentSafetyCheckStage(Stage):
|
||||
"""
|
||||
|
||||
async def initialize(self, ctx: PipelineContext) -> None:
|
||||
self.ctx = ctx
|
||||
config = ctx.astrbot_config["content_safety"]
|
||||
self.strategy_selector = StrategySelector(config)
|
||||
|
||||
@@ -27,12 +29,13 @@ class ContentSafetyCheckStage(Stage):
|
||||
) -> AsyncGenerator[None, None]:
|
||||
"""检查内容安全"""
|
||||
text = check_text if check_text else event.get_message_str()
|
||||
ok, info = self.strategy_selector.check(text)
|
||||
locale = self.ctx.get_current_language()
|
||||
ok, info = self.strategy_selector.check(text, locale=locale)
|
||||
if not ok:
|
||||
if event.is_at_or_wake_command:
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"你的消息或者大模型的响应中包含不适当的内容,已被屏蔽。",
|
||||
t("pipeline.content_blocked", locale=locale),
|
||||
),
|
||||
)
|
||||
yield
|
||||
|
||||
@@ -3,5 +3,5 @@ import abc
|
||||
|
||||
class ContentSafetyStrategy(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def check(self, content: str) -> tuple[bool, str]:
|
||||
def check(self, content: str, locale: str | None = None) -> tuple[bool, str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -4,6 +4,8 @@ from typing import Any, cast
|
||||
|
||||
from aip import AipContentCensor
|
||||
|
||||
from astrbot.core.i18n import t
|
||||
|
||||
from . import ContentSafetyStrategy
|
||||
|
||||
|
||||
@@ -14,7 +16,7 @@ class BaiduAipStrategy(ContentSafetyStrategy):
|
||||
self.secret_key = sk
|
||||
self.client = AipContentCensor(self.app_id, self.api_key, self.secret_key)
|
||||
|
||||
def check(self, content: str) -> tuple[bool, str]:
|
||||
def check(self, content: str, locale: str | None = None) -> tuple[bool, str]:
|
||||
res = self.client.textCensorUserDefined(content)
|
||||
if "conclusionType" not in res:
|
||||
return False, ""
|
||||
@@ -23,10 +25,18 @@ class BaiduAipStrategy(ContentSafetyStrategy):
|
||||
if "data" not in res:
|
||||
return False, ""
|
||||
count = len(res["data"])
|
||||
parts = [f"百度审核服务发现 {count} 处违规:\n"]
|
||||
parts = [
|
||||
t("pipeline.baidu_aip_violation_header", locale=locale, count=count),
|
||||
]
|
||||
for i in res["data"]:
|
||||
# 百度 AIP 返回结构是动态 dict;类型检查时 i 可能被推断为序列,转成 dict 后用 get 取字段
|
||||
parts.append(f"{cast(dict[str, Any], i).get('msg', '')};\n")
|
||||
parts.append("\n判断结果:" + res["conclusion"])
|
||||
parts.append(
|
||||
t(
|
||||
"pipeline.baidu_aip_conclusion",
|
||||
locale=locale,
|
||||
conclusion=res["conclusion"],
|
||||
),
|
||||
)
|
||||
info = "".join(parts)
|
||||
return False, info
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import re
|
||||
|
||||
from astrbot.core.i18n import t
|
||||
|
||||
from . import ContentSafetyStrategy
|
||||
|
||||
|
||||
@@ -17,8 +19,8 @@ class KeywordsStrategy(ContentSafetyStrategy):
|
||||
# json.loads(base64.b64decode(f.read()).decode("utf-8"))["keywords"]
|
||||
# )
|
||||
|
||||
def check(self, content: str) -> tuple[bool, str]:
|
||||
def check(self, content: str, locale: str | None = None) -> tuple[bool, str]:
|
||||
for keyword in self.keywords:
|
||||
if re.search(keyword, content):
|
||||
return False, "内容安全检查不通过,匹配到敏感词。"
|
||||
return False, t("pipeline.keyword_blocked_reason", locale=locale)
|
||||
return True, ""
|
||||
|
||||
@@ -26,9 +26,9 @@ class StrategySelector:
|
||||
),
|
||||
)
|
||||
|
||||
def check(self, content: str) -> tuple[bool, str]:
|
||||
def check(self, content: str, locale: str | None = None) -> tuple[bool, str]:
|
||||
for strategy in self.enabled_strategies:
|
||||
ok, info = strategy.check(content)
|
||||
ok, info = strategy.check(content, locale=locale)
|
||||
if not ok:
|
||||
return False, info
|
||||
return True, ""
|
||||
|
||||
@@ -20,3 +20,6 @@ class PipelineContext:
|
||||
astrbot_config_id: str
|
||||
call_handler = call_handler
|
||||
call_event_hook = call_event_hook
|
||||
|
||||
def get_current_language(self) -> str:
|
||||
return self.plugin_manager.context.get_current_language()
|
||||
|
||||
@@ -5,6 +5,7 @@ from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.i18n import t
|
||||
from astrbot.core.message.message_event_result import MessageEventResult
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.star.star import star_map
|
||||
@@ -62,7 +63,13 @@ class StarRequestSubStage(Stage):
|
||||
)
|
||||
|
||||
if not event.is_stopped() and event.is_at_or_wake_command:
|
||||
ret = f":(\n\n在调用插件 {md.name} 的处理函数 {handler.handler_name} 时出现异常:{e}"
|
||||
ret = t(
|
||||
"pipeline.plugin_handler_error",
|
||||
locale=self.ctx.get_current_language(),
|
||||
plugin_name=md.name,
|
||||
handler_name=handler.handler_name,
|
||||
error=e,
|
||||
)
|
||||
event.set_result(MessageEventResult().message(ret))
|
||||
yield
|
||||
event.clear_result()
|
||||
|
||||
@@ -5,6 +5,7 @@ import traceback
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import file_token_service, html_renderer, logger
|
||||
from astrbot.core.i18n import t
|
||||
from astrbot.core.message.components import At, Image, Json, Node, Plain, Record, Reply
|
||||
from astrbot.core.message.message_event_result import ResultContentType
|
||||
from astrbot.core.pipeline.content_safety_check.stage import ContentSafetyCheckStage
|
||||
@@ -289,7 +290,16 @@ class ResultDecorateStage(Stage):
|
||||
),
|
||||
)
|
||||
else:
|
||||
result.chain.insert(0, Plain(f"🤔 思考: {reasoning_content}\n"))
|
||||
result.chain.insert(
|
||||
0,
|
||||
Plain(
|
||||
t(
|
||||
"pipeline.reasoning_prefix",
|
||||
locale=self.ctx.get_current_language(),
|
||||
reasoning_content=reasoning_content,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if should_tts and tts_provider:
|
||||
new_chain = []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.i18n import t
|
||||
from astrbot.core.message.components import At, AtAll, Reply
|
||||
from astrbot.core.message.message_event_result import MessageChain, MessageEventResult
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
@@ -187,7 +188,12 @@ class WakingCheckStage(Stage):
|
||||
except Exception as e:
|
||||
await event.send(
|
||||
MessageEventResult().message(
|
||||
f"插件 {star_map[handler.handler_module_path].name}: {e}",
|
||||
t(
|
||||
"pipeline.filter_error",
|
||||
locale=self.ctx.get_current_language(),
|
||||
plugin_name=star_map[handler.handler_module_path].name,
|
||||
error=e,
|
||||
),
|
||||
),
|
||||
)
|
||||
event.stop_event()
|
||||
@@ -201,7 +207,11 @@ class WakingCheckStage(Stage):
|
||||
if self.no_permission_reply:
|
||||
await event.send(
|
||||
MessageChain().message(
|
||||
f"您(ID: {event.get_sender_id()})的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。",
|
||||
t(
|
||||
"pipeline.no_permission",
|
||||
locale=self.ctx.get_current_language(),
|
||||
sender_id=event.get_sender_id(),
|
||||
),
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
|
||||
@@ -15,6 +15,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.conversation_mgr import ConversationManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.i18n import normalize_language
|
||||
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.persona_mgr import PersonaManager
|
||||
@@ -438,6 +439,10 @@ class Context:
|
||||
return self._config
|
||||
return self.astrbot_config_mgr.get_conf(umo)
|
||||
|
||||
def get_current_language(self) -> str:
|
||||
"""Get the current runtime reply language."""
|
||||
return normalize_language(self._config.get("language"))
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
session: str | MessageSesion,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 247 icons */
|
||||
/* Auto-generated MDI subset – 248 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -680,6 +680,10 @@
|
||||
content: "\F0B3C";
|
||||
}
|
||||
|
||||
.mdi-numeric-4::before {
|
||||
content: "\F0B3D";
|
||||
}
|
||||
|
||||
.mdi-open-in-new::before {
|
||||
content: "\F03CC";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -987,6 +987,14 @@
|
||||
"name": "System",
|
||||
"system": {
|
||||
"description": "System Settings",
|
||||
"language": {
|
||||
"description": "System Language",
|
||||
"hint": "Language used for AstrBot runtime replies. Currently supports Simplified Chinese and English.",
|
||||
"labels": [
|
||||
"Simplified Chinese",
|
||||
"English"
|
||||
]
|
||||
},
|
||||
"t2i_strategy": {
|
||||
"description": "Text-to-Image Strategy",
|
||||
"hint": "Text-to-image strategy. `remote` uses a remote HTML-based rendering service, `local` uses PIL for local rendering. When using local, place a TTF font named 'font.ttf' in the data/ directory to customize the font."
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
"step3SelectLabel": "Computer Access",
|
||||
"step3Allow": "Allow",
|
||||
"step3Deny": "Disallow",
|
||||
"step4Title": "Choose Language",
|
||||
"step4Desc": "Set AstrBot's preferred language.",
|
||||
"step4SelectLabel": "Language",
|
||||
"step4Chinese": "简体中文",
|
||||
"step4English": "English",
|
||||
"configure": "Configure",
|
||||
"save": "Save",
|
||||
"skip": "Skip",
|
||||
@@ -38,7 +43,10 @@
|
||||
"computerAccessUpdateFailed": "Failed to update Agent computer access",
|
||||
"computerAccessAllowed": "Agent is now allowed to access and use the computer",
|
||||
"computerAccessDenied": "Agent is no longer allowed to access and use the computer",
|
||||
"step3HelpItem3": "For more detailed control, configure the Computer Access options under Settings -> General -> Computer Access."
|
||||
"step3HelpItem3": "For more detailed control, configure the Computer Access options under Settings -> General -> Computer Access.",
|
||||
"languageUpdateFailed": "Failed to update language settings",
|
||||
"languageUpdated": "Language settings updated",
|
||||
"languageRestartConfirm": "AstrBot needs to restart to apply the language change. Restart now?"
|
||||
},
|
||||
"resources": {
|
||||
"title": "Resources",
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
"step3SelectLabel": "Доступ к компьютеру",
|
||||
"step3Allow": "Разрешить",
|
||||
"step3Deny": "Запретить",
|
||||
"step4Title": "Выберите язык",
|
||||
"step4Desc": "Задайте предпочитаемый язык AstrBot.",
|
||||
"step4SelectLabel": "Язык",
|
||||
"step4Chinese": "简体中文",
|
||||
"step4English": "English",
|
||||
"configure": "Настроить",
|
||||
"save": "Сохранить",
|
||||
"skip": "Пропустить",
|
||||
@@ -38,7 +43,10 @@
|
||||
"computerAccessUpdateFailed": "Не удалось обновить доступ Agent к компьютеру",
|
||||
"computerAccessAllowed": "Agent теперь может получать доступ к компьютеру и использовать его",
|
||||
"computerAccessDenied": "Agent больше не может получать доступ к компьютеру и использовать его",
|
||||
"step3HelpItem3": "Для более тонкой настройки перейдите в «Конфигурация → Основные настройки → Доступ к компьютеру»."
|
||||
"step3HelpItem3": "Для более тонкой настройки перейдите в «Конфигурация → Основные настройки → Доступ к компьютеру».",
|
||||
"languageUpdateFailed": "Не удалось обновить настройки языка",
|
||||
"languageUpdated": "Настройки языка обновлены",
|
||||
"languageRestartConfirm": "Чтобы применить смену языка, AstrBot нужно перезапустить. Перезапустить сейчас?"
|
||||
},
|
||||
"resources": {
|
||||
"title": "Ресурсы",
|
||||
|
||||
@@ -989,6 +989,14 @@
|
||||
"name": "系统配置",
|
||||
"system": {
|
||||
"description": "系统配置",
|
||||
"language": {
|
||||
"description": "系统语言",
|
||||
"hint": "用于 AstrBot 运行时回复的语言。目前支持简体中文和英文。",
|
||||
"labels": [
|
||||
"简体中文",
|
||||
"English"
|
||||
]
|
||||
},
|
||||
"t2i_strategy": {
|
||||
"description": "文本转图像策略",
|
||||
"hint": "文本转图像策略。`remote` 为使用远程基于 HTML 的渲染服务,`local` 为使用 PIL 本地渲染。当使用 local 时,将 ttf 字体命名为 'font.ttf' 放在 data/ 目录下可自定义字体。"
|
||||
@@ -1643,4 +1651,4 @@
|
||||
"helpMiddle": "或",
|
||||
"helpSuffix": "。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
"step3SelectLabel": "电脑访问权限",
|
||||
"step3Allow": "允许",
|
||||
"step3Deny": "不允许",
|
||||
"step4Title": "选择语言",
|
||||
"step4Desc": "设置 AstrBot 首选语言。",
|
||||
"step4SelectLabel": "语言",
|
||||
"step4Chinese": "简体中文",
|
||||
"step4English": "English",
|
||||
"configure": "去配置",
|
||||
"save": "保存",
|
||||
"skip": "跳过",
|
||||
@@ -38,7 +43,10 @@
|
||||
"computerAccessUpdateFailed": "更新 Agent 电脑访问权限失败",
|
||||
"computerAccessAllowed": "已允许 Agent 访问和使用电脑",
|
||||
"computerAccessDenied": "已禁止 Agent 访问和使用电脑",
|
||||
"step3HelpItem3": "如需更多细化权限与能力配置,可前往 配置文件 -> 普通配置 -> 使用电脑能力 继续设置。"
|
||||
"step3HelpItem3": "如需更多细化权限与能力配置,可前往 配置文件 -> 普通配置 -> 使用电脑能力 继续设置。",
|
||||
"languageUpdateFailed": "更新语言设置失败",
|
||||
"languageUpdated": "语言设置已更新",
|
||||
"languageRestartConfirm": "应用语言切换之后需要重启 AstrBot。是否立即重启?"
|
||||
},
|
||||
"resources": {
|
||||
"title": "相关资源",
|
||||
|
||||
@@ -89,6 +89,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</v-timeline-item>
|
||||
|
||||
<v-timeline-item :dot-color="languageStepState === 'completed' ? 'success' : 'primary'"
|
||||
icon="mdi-numeric-4" fill-dot size="small">
|
||||
<div class="pl-2">
|
||||
<div class="text-h6 font-weight-bold mb-1">{{ tm('onboard.step4Title') }}</div>
|
||||
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('onboard.step4Desc') }}</p>
|
||||
<div class="d-flex flex-wrap align-center ga-3">
|
||||
<v-select
|
||||
v-model="selectedLanguage"
|
||||
:items="languageOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:label="tm('onboard.step4SelectLabel')"
|
||||
:loading="savingLanguage"
|
||||
:disabled="savingLanguage"
|
||||
hide-details
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
class="language-select"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</v-card>
|
||||
|
||||
@@ -188,6 +211,7 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<WaitingForRestart ref="wfr" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -195,23 +219,34 @@
|
||||
import { computed, ref, watch, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import type { Locale } from '@/i18n/types';
|
||||
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog';
|
||||
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot';
|
||||
import { useToast } from '@/utils/toast';
|
||||
import { MarkdownRender } from 'markstream-vue';
|
||||
import 'markstream-vue/index.css';
|
||||
|
||||
type StepState = 'pending' | 'completed' | 'skipped';
|
||||
type ComputerAccessRuntime = 'local' | 'none';
|
||||
type CoreLanguage = 'zh-CN' | 'en-US';
|
||||
type WaitingForRestartRef = {
|
||||
check: (initialStartTime?: number | null) => void | Promise<void>;
|
||||
stop?: () => void;
|
||||
};
|
||||
|
||||
const { tm } = useModuleI18n('features/welcome');
|
||||
const { locale } = useI18n();
|
||||
const { locale, setLocale } = useI18n();
|
||||
const { success: showSuccess, error: showError } = useToast();
|
||||
const confirmDialog = useConfirmDialog();
|
||||
|
||||
const showAddPlatformDialog = ref(false);
|
||||
const showProviderDialog = ref(false);
|
||||
const showComputerAccessHelpDialog = ref(false);
|
||||
const loadingPlatformDialog = ref(false);
|
||||
const wfr = ref<WaitingForRestartRef | null>(null);
|
||||
|
||||
const platformMetadata = ref<Record<string, any>>({});
|
||||
const platformConfigData = ref<Record<string, any>>({});
|
||||
@@ -224,6 +259,10 @@ const computerAccessStepState = ref<StepState>('pending');
|
||||
const computerAccessRuntime = ref<ComputerAccessRuntime>('none');
|
||||
const savedComputerAccessRuntime = ref<ComputerAccessRuntime>('none');
|
||||
const savingComputerAccess = ref(false);
|
||||
const languageStepState = ref<StepState>('pending');
|
||||
const selectedLanguage = ref<CoreLanguage>('zh-CN');
|
||||
const savedLanguage = ref<CoreLanguage>('zh-CN');
|
||||
const savingLanguage = ref(false);
|
||||
const welcomeAnnouncementRaw = ref<unknown>(null);
|
||||
|
||||
function resolveWelcomeAnnouncement(raw: unknown, currentLocale: string) {
|
||||
@@ -419,6 +458,11 @@ const computerAccessOptions = computed(() => [
|
||||
{ title: tm('onboard.step3Deny'), value: 'none' }
|
||||
]);
|
||||
|
||||
const languageOptions = computed(() => [
|
||||
{ title: tm('onboard.step4Chinese'), value: 'zh-CN' },
|
||||
{ title: tm('onboard.step4English'), value: 'en-US' }
|
||||
]);
|
||||
|
||||
async function saveComputerAccessRuntime() {
|
||||
savingComputerAccess.value = true;
|
||||
try {
|
||||
@@ -453,6 +497,62 @@ async function saveComputerAccessRuntime() {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCoreLanguage(value: unknown): CoreLanguage {
|
||||
return value === 'en-US' ? 'en-US' : 'zh-CN';
|
||||
}
|
||||
|
||||
async function syncLanguage(configData: any) {
|
||||
const normalizedLanguage = normalizeCoreLanguage(configData?.language);
|
||||
selectedLanguage.value = normalizedLanguage;
|
||||
savedLanguage.value = normalizedLanguage;
|
||||
languageStepState.value = 'completed';
|
||||
|
||||
if (locale.value !== normalizedLanguage) {
|
||||
await setLocale(normalizedLanguage as Locale);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptRestartAfterLanguageChange() {
|
||||
const shouldRestart = await askForConfirmation(
|
||||
tm('onboard.languageRestartConfirm'),
|
||||
confirmDialog
|
||||
);
|
||||
if (!shouldRestart) return;
|
||||
|
||||
try {
|
||||
await restartAstrBotRuntime(wfr.value);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLanguage() {
|
||||
savingLanguage.value = true;
|
||||
try {
|
||||
const configData = await fetchDefaultConfig();
|
||||
configData.language = selectedLanguage.value;
|
||||
|
||||
const updateRes = await axios.post('/api/config/astrbot/update', {
|
||||
conf_id: 'default',
|
||||
config: configData
|
||||
});
|
||||
if (updateRes.data.status !== 'ok') {
|
||||
throw new Error(updateRes.data.message || tm('onboard.languageUpdateFailed'));
|
||||
}
|
||||
|
||||
savedLanguage.value = selectedLanguage.value;
|
||||
languageStepState.value = 'completed';
|
||||
await setLocale(selectedLanguage.value as Locale);
|
||||
showSuccess(tm('onboard.languageUpdated'));
|
||||
await promptRestartAfterLanguageChange();
|
||||
} catch (err: any) {
|
||||
selectedLanguage.value = savedLanguage.value;
|
||||
showError(err?.response?.data?.message || err?.message || tm('onboard.languageUpdateFailed'));
|
||||
} finally {
|
||||
savingLanguage.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWelcomeAnnouncement() {
|
||||
try {
|
||||
const res = await axios.get('https://cloud.astrbot.app/api/v1/announcement');
|
||||
@@ -487,6 +587,7 @@ onMounted(async () => {
|
||||
try {
|
||||
const defaultConfig = await fetchDefaultConfig();
|
||||
syncComputerAccessRuntime(defaultConfig);
|
||||
await syncLanguage(defaultConfig);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -552,6 +653,18 @@ watch(computerAccessRuntime, async (value, oldValue) => {
|
||||
computerAccessRuntime.value = savedComputerAccessRuntime.value;
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectedLanguage, async (value, oldValue) => {
|
||||
if (value === oldValue) return;
|
||||
if (value === savedLanguage.value) return;
|
||||
if (savingLanguage.value) return;
|
||||
|
||||
try {
|
||||
await saveLanguage();
|
||||
} catch {
|
||||
selectedLanguage.value = savedLanguage.value;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -572,6 +685,11 @@ watch(computerAccessRuntime, async (value, oldValue) => {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
max-width: 240px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.computer-access-help-list {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
|
||||
Reference in New Issue
Block a user