Compare commits

...

49 Commits

Author SHA1 Message Date
Soulter
e9be8cf69f chore: bump version to 4.7.4 2025-12-01 18:42:07 +08:00
Soulter
31d53edb9d refactor: standardize provider test method implementation
- Updated the `test` method in all provider classes to remove return values and raise exceptions for failure cases, enhancing clarity and consistency.
- Adjusted related logic in the dashboard and command routes to align with the new `test` method behavior, simplifying error handling.
2025-12-01 18:37:08 +08:00
Soulter
2ba0460f19 feat: introduce file extract capability (#3870)
* feat: introduce file extract capability

powered by MoonshotAI

* fix: correct indentation in default configuration file

* fix: add error handling for file extract application in InternalAgentSubStage

* fix: update file name handling in InternalAgentSubStage to correctly associate file names with extracted content

* feat: add condition settings for local agent runner in default configuration

* fix: enhance file naming logic in File component and update prompt handling in InternalAgentSubStage
2025-12-01 18:12:39 +08:00
雪語
0e034f0fbd fix: aiocqhttp 适配器 NapCat 文件名获取为空 (#3853)
* aiocqhttp 适配器 NapCat 文件名获取为空

修复使用 NapCat 时,文件消息的 File.name 为空的问题。原代码硬编码 name="",导致下游插件无法获取文件名和扩展名

* Enhance file name retrieval from message data

Updated file name extraction logic to check multiple fields for better accuracy.
2025-12-01 13:36:19 +08:00
Soulter
2a7d03f9e1 fix: fit language and log AI responses more clearly (#3864)
* fix: fit language and log AI responses more clearly

* chore: ruff format
2025-12-01 13:24:52 +08:00
Soulter
72fac4b9f1 feat: implement unified provider availability testing across components (#3865)
- Added a `test` method to each provider class to standardize availability checks.
- Updated the dashboard and command routes to utilize the new `test` method for provider reachability verification, simplifying the logic and improving maintainability.
- Removed redundant reachability check logic from the command handler.
2025-12-01 13:17:20 +08:00
Soulter
38281ba2cf refactor: restore reachability check configuration in default settings and localization files 2025-12-01 00:38:30 +08:00
Soulter
21aa3174f4 fix: disable reachability check in default configuration 2025-12-01 00:16:11 +08:00
邹永赫
dcda871fc0 feat: provider availability reachability improvements (#3708) 2025-12-01 01:06:10 +09:00
Soulter
c13c51f499 fix: assistant message validation error when tool_call exists but content not exists (#3862)
* fix: assistant message validation error when tool_call exists but content not exists

* fix: enhance content validation in Message model to allow None for assistant role with tool_calls
2025-11-30 23:42:37 +08:00
Dt8333
a130db5cf4 fix: 将 Graceful shutdown 的异常改为 KeyboardInterrupt (#3855) 2025-11-30 20:31:17 +08:00
邹永赫
7faeb5cea8 Merge pull request #3850 from zouyonghe/feature/plugin-upgrade-all
增加升级所有插件按钮
2025-11-30 15:12:36 +09:00
ZouYonghe
8d3ff61e0d Format plugin route with ruff 2025-11-30 11:56:24 +08:00
ZouYonghe
4c03e82570 Fix plugin update JSON parsing and concurrency handling 2025-11-30 11:50:46 +08:00
ZouYonghe
e7e8664ab4 chore: tweak update all label 2025-11-30 11:18:30 +08:00
ZouYonghe
1dd1623e7d feat: batch update plugins via new api 2025-11-30 11:11:36 +08:00
ZouYonghe
80d8161d58 feat: add update all plugins action 2025-11-30 10:40:46 +08:00
Soulter
fc80d7d681 chore: bump version to 4.7.3 2025-11-30 00:42:49 +08:00
Soulter
c2f036b27c chore: bump vertion to 4.7.2 2025-11-30 00:33:07 +08:00
Soulter
4087bbb512 perf: set content attribute optional to AssistantMessageSegment for enhanced message handling
fixes: #3843
2025-11-30 00:32:00 +08:00
Soulter
e1c728582d chore: bump version to 4.7.2 2025-11-30 00:18:23 +08:00
Oscar Shaw
93c69a639a feat: 新增群聊模式下的专用图片转述模型配置 (#3822)
* feat: add image caption provider configuration for group chat

- Introduced `image_caption_provider_id` to allow separate configuration for group chat image understanding.
- Updated metadata and hints in English and Chinese for clarity on new settings.
- Adjusted logic in long term memory to utilize the new provider ID for image captioning.

* fix: format

* Fix logic for image caption and active reply settings

* Fix indentation and formatting in long_term_memory.py

* chore: ruff format

---------

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>
2025-11-29 23:53:32 +08:00
Soulter
a7fdc98b29 fix: third party agent runner cannot run properly when using non-default config file
fix: #3815
2025-11-29 23:45:12 +08:00
Soulter
85b7f104df fix: remove unnecessary provider check (#3846)
fixes: #3815
2025-11-29 23:15:19 +08:00
Oscar Shaw
d76d1bd7fe perf: adjust padding for PlatformPage and ProviderPage log sections (#3825)
- Added bottom margin to log card for better spacing.
2025-11-29 19:15:35 +08:00
Soulter
df4412aa80 style: adjust bot-embedded-image max-width and remove hover effect for improved layout 2025-11-29 01:31:25 +08:00
Soulter
ab2c94e19a chore: comment out error logging in provider sources to reduce verbosity 2025-11-28 19:59:33 +08:00
Oscar Shaw
37cc4e2121 perf: console tag UI improve (#3816)
- Added yarn.lock to .gitignore to prevent tracking of Yarn lock files.
- Updated ConsoleDisplayer.vue to improve chip styling
2025-11-28 17:17:11 +08:00
Soulter
60dfdd0a66 chore: update astrbot cli version 2025-11-28 16:53:20 +08:00
Soulter
bb8b2cb194 chore: bump version to 4.7.1 2025-11-28 15:13:35 +08:00
Soulter
4e29684aa3 fix: add plugin set and knowledge bases selection in custom rules page (#3813)
fixes: #3806
2025-11-28 13:29:50 +08:00
Soulter
0e17e3553d chore: bump version to 4.7.0 2025-11-27 23:50:05 +08:00
Soulter
0a55060e89 fix: session controller in webchat 2025-11-27 22:32:35 +08:00
Soulter
77859c7daa feat: enhance provider status display in ProviderPage
- Added a tooltip to show detailed provider status, including availability and error messages.
- Refactored item details template to include status chips for better visual representation.
- Removed unused status section to streamline the UI.
2025-11-27 16:39:51 +08:00
Soulter
ba39c393a0 perf: enhance provider management with reload locking and logging (#3793)
- Introduced a reload lock to prevent concurrent reloads of providers.
- Added logging to indicate when a provider is disabled and when providers are being synchronized with the configuration.
- Refactored the reload method to improve clarity and maintainability.


Co-authored-by: anka <1350989414@qq.com>
2025-11-27 16:25:31 +08:00
Soulter
6a50d316d9 fix: mcp server cannot reload successfully after updating mcp server config (#3797)
fixes: #3780
2025-11-27 16:22:26 +08:00
Soulter
88c1d77f0b perf: add at message to group chat history (#3796)
* feat: enhance long-term memory message formatting

- Added support for 'At' message components in long-term memory, allowing for better representation of mentions in messages.

* chore: ruff check
2025-11-27 15:59:07 +08:00
Dt8333
758ce40cc1 chore: fix test (#3787) 2025-11-27 14:02:42 +08:00
Soulter
3e7bb80492 chore: ruff format 2025-11-27 14:01:25 +08:00
Soulter
75e95aa9ca fix: update session management icon in sidebar
- Changed the icon for the session management sidebar item from 'mdi-account-group' to 'mdi-pencil-ruler' for better representation.
2025-11-27 14:00:05 +08:00
Soulter
a389842e25 feat: update session management UI with information button and layout adjustments
- Added an information button linking to custom rules documentation.
- Adjusted layout for improved spacing and readability in the session management page.
- Minor refactoring of the data table component for better alignment.
2025-11-27 13:58:37 +08:00
Soulter
0f6a3c3f5a refactor: session management custom rules (#3792)
* refactor: umo custom rules

* feat(i18n): update session management translations and improve provider configuration handling

- Updated English and Chinese translations for session management, including "Unified Message Origin" and "Follow Config".
- Enhanced provider configuration options to include "Follow Config" as a selectable item.
- Removed unused clear buttons and refactored provider configuration saving logic to handle updates and deletions more efficiently.
2025-11-27 13:30:43 +08:00
Soulter
133f27422d feat: implement i18n of astrbot config (#3772)
* feat: implement i18n of astrbot config

* feat(config): update configuration metadata with i18n details and future deprecation notes
2025-11-26 16:40:58 +08:00
RC-CHN
abc6deb244 feat: add plugin logo placeholder (#3784) 2025-11-26 16:22:11 +08:00
teapot1de
06869b4597 docs: clarify segmented_reply words_count_threshold hint (#3779)
Update the configuration hint for `words_count_threshold` to explicitly state that it acts as a maximum limit for segmentation, preventing user confusion about it being a minimum trigger.
2025-11-26 16:15:09 +08:00
dependabot[bot]
d32cea9870 chore(deps): bump actions/checkout in the github-actions group (#3775)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 16:13:42 +08:00
Soulter
4b68100f16 feat(chat): add standalone chat component and integrate with config page for testing configurations (#3767)
* feat(chat): add standalone chat component and integrate with config page for testing configurations

* feat(chat): add error handling for message sending and session creation
2025-11-24 22:06:02 +08:00
Soulter
5c5515d462 fix: segmented reply regex error handling (#3771)
* fix: segmented reply regex error handling

closes: #3761

* fix: improve regex handling for segmented replies to support multiline input

* fix: update regex handling in ResultDecorateStage to use findall for segmented replies

* fix: update error logging message for segmented reply regex handling
2025-11-24 22:00:59 +08:00
Soulter
3932b8f982 Merge pull request #3760 from AstrBotDevs/feat/agent-runner
refactor: transfer dify, coze and alibaba dashscope from chat provider to agent runner
2025-11-24 15:33:20 +08:00
80 changed files with 3975 additions and 2674 deletions

View File

@@ -13,7 +13,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Dashboard Build
run: |
@@ -70,7 +70,7 @@ jobs:
needs: build-and-publish-to-github-release
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6

View File

@@ -56,7 +56,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6

View File

@@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tag: true
@@ -118,7 +118,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tag: true

3
.gitignore vendored
View File

@@ -34,6 +34,7 @@ dashboard/node_modules/
dashboard/dist/
package-lock.json
package.json
yarn.lock
# Operating System
**/.DS_Store
@@ -47,3 +48,5 @@ astrbot.lock
chroma
venv/*
pytest.ini
AGENTS.md
IFLOW.md

View File

@@ -1 +1 @@
__version__ = "3.5.23"
__version__ = "4.7.4"

View File

@@ -345,9 +345,6 @@ class MCPClient:
async def cleanup(self):
"""Clean up resources including old exit stacks from reconnections"""
# Set running_event first to unblock any waiting tasks
self.running_event.set()
# Close current exit stack
try:
await self.exit_stack.aclose()
@@ -359,6 +356,9 @@ class MCPClient:
# Just clear the list to release references
self._old_exit_stacks.clear()
# Set running_event first to unblock any waiting tasks
self.running_event.set()
class MCPTool(FunctionTool, Generic[TContext]):
"""A function tool that calls an MCP service."""

View File

@@ -3,7 +3,7 @@
from typing import Any, ClassVar, Literal, cast
from pydantic import BaseModel, GetCoreSchemaHandler
from pydantic import BaseModel, GetCoreSchemaHandler, model_validator
from pydantic_core import core_schema
@@ -145,22 +145,39 @@ class Message(BaseModel):
"tool",
]
content: str | list[ContentPart]
content: str | list[ContentPart] | None = None
"""The content of the message."""
tool_calls: list[ToolCall] | list[dict] | None = None
"""The tool calls of the message."""
tool_call_id: str | None = None
"""The ID of the tool call."""
@model_validator(mode="after")
def check_content_required(self):
# assistant + tool_calls is not None: allow content to be None
if self.role == "assistant" and self.tool_calls is not None:
return self
# other all cases: content is required
if self.content is None:
raise ValueError(
"content is required unless role='assistant' and tool_calls is not None"
)
return self
class AssistantMessageSegment(Message):
"""A message segment from the assistant."""
role: Literal["assistant"] = "assistant"
tool_calls: list[ToolCall] | list[dict] | None = None
class ToolCallMessageSegment(Message):
"""A message segment representing a tool call."""
role: Literal["tool"] = "tool"
tool_call_id: str
class UserMessageSegment(Message):

View File

@@ -4,7 +4,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.6.1"
VERSION = "4.7.4"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
# 默认配置
@@ -73,8 +73,14 @@ DEFAULT_CONFIG = {
"coze_agent_runner_provider_id": "",
"dashscope_agent_runner_provider_id": "",
"unsupported_streaming_strategy": "realtime_segmenting",
"reachability_check": False,
"max_agent_step": 30,
"tool_call_timeout": 60,
"file_extract": {
"enable": False,
"provider": "moonshotai",
"moonshotai_api_key": "",
},
},
"provider_stt_settings": {
"enable": False,
@@ -90,6 +96,7 @@ DEFAULT_CONFIG = {
"group_icl_enable": False,
"group_message_max_cnt": 300,
"image_caption": False,
"image_caption_provider_id": "",
"active_reply": {
"enable": False,
"method": "possibility_reply",
@@ -145,7 +152,16 @@ DEFAULT_CONFIG = {
}
# 配置项的中文描述、值类型
"""
AstrBot v3 时代的配置元数据,目前仅承担以下功能:
1. 保存配置时,配置项的类型验证
2. WebUI 展示提供商和平台适配器模版
WebUI 的配置文件在 `CONFIG_METADATA_3` 中。
未来将会逐步淘汰此配置元数据。
"""
CONFIG_METADATA_2 = {
"platform_group": {
"metadata": {
@@ -638,7 +654,7 @@ CONFIG_METADATA_2 = {
},
"words_count_threshold": {
"type": "int",
"hint": "超过这个字数的消息会被分段回复。默认为 150",
"hint": "分段回复的字数上限。只有字数小于此值的消息会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
},
"regex": {
"type": "string",
@@ -1091,7 +1107,7 @@ CONFIG_METADATA_2 = {
"api_base": "",
"model": "whisper-1",
},
"Whisper(本地加载)": {
"Whisper(Local)": {
"hint": "启用前请 pip 安装 openai-whisper 库N卡用户大约下载 2GB主要是 torch 和 cudaCPU 用户大约下载 1 GB并且安装 ffmpeg。否则将无法正常转文字。",
"provider": "openai",
"type": "openai_whisper_selfhost",
@@ -1100,7 +1116,7 @@ CONFIG_METADATA_2 = {
"id": "whisper_selfhost",
"model": "tiny",
},
"SenseVoice(本地加载)": {
"SenseVoice(Local)": {
"hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库默认使用CPU大约下载 1 GB并且安装 ffmpeg。否则将无法正常转文字。",
"type": "sensevoice_stt_selfhost",
"provider": "sensevoice",
@@ -1135,7 +1151,7 @@ CONFIG_METADATA_2 = {
"pitch": "+0Hz",
"timeout": 20,
},
"GSV TTS(本地加载)": {
"GSV TTS(Local)": {
"id": "gsv_tts",
"enable": False,
"provider": "gpt_sovits",
@@ -2058,6 +2074,20 @@ CONFIG_METADATA_2 = {
"tool_call_timeout": {
"type": "int",
},
"file_extract": {
"type": "object",
"items": {
"enable": {
"type": "bool",
},
"provider": {
"type": "string",
},
"moonshotai_api_key": {
"type": "string",
},
},
},
},
},
"provider_stt_settings": {
@@ -2100,6 +2130,9 @@ CONFIG_METADATA_2 = {
"image_caption": {
"type": "bool",
},
"image_caption_provider_id": {
"type": "string",
},
"image_caption_prompt": {
"type": "string",
},
@@ -2189,6 +2222,14 @@ CONFIG_METADATA_2 = {
}
"""
v4.7.0 之后name, description, hint 等字段已经实现 i18n 国际化。国际化资源文件位于:
- dashboard/src/i18n/locales/en-US/features/config-metadata.json
- dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
如果在此文件中添加了新的配置字段,请务必同步更新上述两个国际化资源文件。
"""
CONFIG_METADATA_3 = {
"ai_group": {
"name": "AI 配置",
@@ -2381,6 +2422,36 @@ CONFIG_METADATA_3 = {
"provider_settings.enable": True,
},
},
# "file_extract": {
# "description": "文档解析能力 [beta]",
# "type": "object",
# "items": {
# "provider_settings.file_extract.enable": {
# "description": "启用文档解析能力",
# "type": "bool",
# },
# "provider_settings.file_extract.provider": {
# "description": "文档解析提供商",
# "type": "string",
# "options": ["moonshotai"],
# "condition": {
# "provider_settings.file_extract.enable": True,
# },
# },
# "provider_settings.file_extract.moonshotai_api_key": {
# "description": "Moonshot AI API Key",
# "type": "string",
# "condition": {
# "provider_settings.file_extract.provider": "moonshotai",
# "provider_settings.file_extract.enable": True,
# },
# },
# },
# "condition": {
# "provider_settings.agent_runner_type": "local",
# "provider_settings.enable": True,
# },
# },
"others": {
"description": "其他配置",
"type": "object",
@@ -2475,6 +2546,11 @@ CONFIG_METADATA_3 = {
"description": "开启 TTS 时同时输出语音和文字内容",
"type": "bool",
},
"provider_settings.reachability_check": {
"description": "提供商可达性检测",
"type": "bool",
"hint": "/provider 命令列出模型时是否并发检测连通性。开启后会主动调用模型测试连通性,可能产生额外 token 消耗。",
},
},
"condition": {
"provider_settings.enable": True,
@@ -2768,7 +2844,16 @@ CONFIG_METADATA_3 = {
"provider_ltm_settings.image_caption": {
"description": "自动理解图片",
"type": "bool",
"hint": "需要设置默认图片转述模型。",
"hint": "需要设置群聊图片转述模型。",
},
"provider_ltm_settings.image_caption_provider_id": {
"description": "群聊图片转述模型",
"type": "string",
"_special": "select_provider",
"hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。",
"condition": {
"provider_ltm_settings.image_caption": True,
},
},
"provider_ltm_settings.active_reply.enable": {
"description": "主动回复",

View File

@@ -0,0 +1,110 @@
"""
配置元数据国际化工具
提供配置元数据的国际化键转换功能
"""
from typing import Any
class ConfigMetadataI18n:
"""配置元数据国际化转换器"""
@staticmethod
def _get_i18n_key(group: str, section: str, field: str, attr: str) -> str:
"""
生成国际化键
Args:
group: 配置组,如 'ai_group', 'platform_group'
section: 配置节,如 'agent_runner', 'general'
field: 字段名,如 'enable', 'default_provider'
attr: 属性类型,如 'description', 'hint', 'labels'
Returns:
国际化键,格式如: 'ai_group.agent_runner.enable.description'
"""
if field:
return f"{group}.{section}.{field}.{attr}"
else:
return f"{group}.{section}.{attr}"
@staticmethod
def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, Any]:
"""
将配置元数据转换为使用国际化键
Args:
metadata: 原始配置元数据字典
Returns:
使用国际化键的配置元数据字典
"""
result = {}
for group_key, group_data in metadata.items():
group_result = {
"name": f"{group_key}.name",
"metadata": {},
}
for section_key, section_data in group_data.get("metadata", {}).items():
section_result = {
"description": f"{group_key}.{section_key}.description",
"type": section_data.get("type"),
}
# 复制其他属性
for key in ["items", "condition", "_special", "invisible"]:
if key in section_data:
section_result[key] = section_data[key]
# 处理 hint
if "hint" in section_data:
section_result["hint"] = f"{group_key}.{section_key}.hint"
# 处理 items 中的字段
if "items" in section_data and isinstance(section_data["items"], dict):
items_result = {}
for field_key, field_data in section_data["items"].items():
# 处理嵌套的点号字段名(如 provider_settings.enable
field_name = field_key
field_result = {}
# 复制基本属性
for attr in [
"type",
"condition",
"_special",
"invisible",
"options",
]:
if attr in field_data:
field_result[attr] = field_data[attr]
# 转换文本属性为国际化键
if "description" in field_data:
field_result["description"] = (
f"{group_key}.{section_key}.{field_name}.description"
)
if "hint" in field_data:
field_result["hint"] = (
f"{group_key}.{section_key}.{field_name}.hint"
)
if "labels" in field_data:
field_result["labels"] = (
f"{group_key}.{section_key}.{field_name}.labels"
)
items_result[field_key] = field_result
section_result["items"] = items_result
group_result["metadata"][section_key] = section_result
result[group_key] = group_result
return result

View File

@@ -722,7 +722,12 @@ class File(BaseMessageComponent):
"""下载文件"""
download_dir = os.path.join(get_astrbot_data_path(), "temp")
os.makedirs(download_dir, exist_ok=True)
file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}")
if self.name:
name, ext = os.path.splitext(self.name)
filename = f"{name}_{uuid.uuid4().hex[:8]}{ext}"
else:
filename = f"{uuid.uuid4().hex}"
file_path = os.path.join(download_dir, filename)
await download_file(self.url, file_path)
self.file_ = os.path.abspath(file_path)

View File

@@ -9,7 +9,7 @@ from astrbot.core import logger
from astrbot.core.agent.tool import ToolSet
from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.core.conversation_mgr import Conversation
from astrbot.core.message.components import Image
from astrbot.core.message.components import File, Image, Reply
from astrbot.core.message.message_event_result import (
MessageChain,
MessageEventResult,
@@ -22,6 +22,7 @@ from astrbot.core.provider.entities import (
ProviderRequest,
)
from astrbot.core.star.star_handler import EventType, star_map
from astrbot.core.utils.file_extract import extract_file_moonshotai
from astrbot.core.utils.metrics import Metric
from astrbot.core.utils.session_lock import session_lock_manager
@@ -56,6 +57,13 @@ class InternalAgentSubStage(Stage):
self.show_reasoning = settings.get("display_reasoning_text", False)
self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False)
file_extract_conf: dict = settings.get("file_extract", {})
self.file_extract_enabled: bool = file_extract_conf.get("enable", False)
self.file_extract_prov: str = file_extract_conf.get("provider", "moonshotai")
self.file_extract_msh_api_key: str = file_extract_conf.get(
"moonshotai_api_key", ""
)
self.conv_manager = ctx.plugin_manager.context.conversation_manager
def _select_provider(self, event: AstrMessageEvent):
@@ -114,6 +122,50 @@ class InternalAgentSubStage(Stage):
req.func_tool = ToolSet()
req.func_tool.add_tool(KNOWLEDGE_BASE_QUERY_TOOL)
async def _apply_file_extract(
self,
event: AstrMessageEvent,
req: ProviderRequest,
):
"""Apply file extract to the provider request"""
file_paths = []
file_names = []
for comp in event.message_obj.message:
if isinstance(comp, File):
file_paths.append(await comp.get_file())
file_names.append(comp.name)
elif isinstance(comp, Reply) and comp.chain:
for reply_comp in comp.chain:
if isinstance(reply_comp, File):
file_paths.append(await reply_comp.get_file())
file_names.append(reply_comp.name)
if not file_paths:
return
if not req.prompt:
req.prompt = "总结一下文件里面讲了什么?"
if self.file_extract_prov == "moonshotai":
if not self.file_extract_msh_api_key:
logger.error("Moonshot AI API key for file extract is not set")
return
file_contents = await asyncio.gather(
*[
extract_file_moonshotai(file_path, self.file_extract_msh_api_key)
for file_path in file_paths
]
)
else:
logger.error(f"Unsupported file extract provider: {self.file_extract_prov}")
return
# add file extract results to contexts
for file_content, file_name in zip(file_contents, file_names):
req.contexts.append(
{
"role": "system",
"content": f"File Extract Results of user uploaded files:\n{file_content}\nFile Name: {file_name or 'Unknown'}",
},
)
def _truncate_contexts(
self,
contexts: list[dict],
@@ -346,6 +398,17 @@ class InternalAgentSubStage(Stage):
event.set_extra("provider_request", req)
# fix contexts json str
if isinstance(req.contexts, str):
req.contexts = json.loads(req.contexts)
# apply file extract
if self.file_extract_enabled:
try:
await self._apply_file_extract(event, req)
except Exception as e:
logger.error(f"Error occurred while applying file extract: {e}")
if not req.prompt and not req.image_urls:
return
@@ -356,10 +419,6 @@ class InternalAgentSubStage(Stage):
# apply knowledge base feature
await self._apply_kb(event, req)
# fix contexts json str
if isinstance(req.contexts, str):
req.contexts = json.loads(req.contexts)
# truncate contexts to fit max length
if req.contexts:
req.contexts = self._truncate_contexts(req.contexts)

View File

@@ -2,7 +2,7 @@ import asyncio
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING
from astrbot.core import logger
from astrbot.core import astrbot_config, logger
from astrbot.core.agent.runners.coze.coze_agent_runner import CozeAgentRunner
from astrbot.core.agent.runners.dashscope.dashscope_agent_runner import (
DashscopeAgentRunner,
@@ -88,12 +88,15 @@ class ThirdPartyAgentSubStage(Stage):
return
self.prov_cfg: dict = next(
(p for p in self.conf["provider"] if p["id"] == self.prov_id),
(p for p in astrbot_config["provider"] if p["id"] == self.prov_id),
{},
)
if not self.prov_id or not self.prov_cfg:
if not self.prov_id:
logger.error("没有填写 Agent Runner 提供商 ID请前往配置页面配置。")
return
if not self.prov_cfg:
logger.error(
"Third Party Agent Runner provider ID is not configured properly."
f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。"
)
return

View File

@@ -1,6 +1,5 @@
from collections.abc import AsyncGenerator
from astrbot.core import logger
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.provider.entities import ProviderRequest
from astrbot.core.star.star_handler import StarHandlerMetadata
@@ -63,12 +62,5 @@ class ProcessStage(Stage):
if (
event.get_result() and not event.get_result().is_stopped()
) or not event.get_result():
# 事件没有终止传播
provider = self.ctx.plugin_manager.context.get_using_provider()
if not provider:
logger.info("未找到可用的 LLM 提供商,请先前往配置服务提供商。")
return
async for _ in self.agent_sub_stage.process(event):
yield

View File

@@ -161,11 +161,21 @@ class ResultDecorateStage(Stage):
# 不分段回复
new_chain.append(comp)
continue
split_response = re.findall(
self.regex,
comp.text,
re.DOTALL | re.MULTILINE,
)
try:
split_response = re.findall(
self.regex,
comp.text,
re.DOTALL | re.MULTILINE,
)
except re.error:
logger.error(
f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
)
split_response = re.findall(
r".*?[。?!~…]+|.+$",
comp.text,
re.DOTALL | re.MULTILINE,
)
if not split_response:
new_chain.append(comp)
continue

View File

@@ -246,7 +246,13 @@ class AiocqhttpAdapter(Platform):
if m["data"].get("url") and m["data"].get("url").startswith("http"):
# Lagrange
logger.info("guessing lagrange")
file_name = m["data"].get("file_name", "file")
# 检查多个可能的文件名字段
file_name = (
m["data"].get("file_name", "")
or m["data"].get("name", "")
or m["data"].get("file", "")
or "file"
)
abm.message.append(File(name=file_name, url=m["data"]["url"]))
else:
try:
@@ -265,7 +271,14 @@ class AiocqhttpAdapter(Platform):
)
if ret and "url" in ret:
file_url = ret["url"] # https
a = File(name="", url=file_url)
# 优先从 API 返回值获取文件名,其次从原始消息数据获取
file_name = (
ret.get("file_name", "")
or ret.get("name", "")
or m["data"].get("file", "")
or m["data"].get("file_name", "")
)
a = File(name=file_name, url=file_url)
abm.message.append(a)
else:
logger.error(f"获取文件失败: {ret}")

View File

@@ -250,7 +250,7 @@ class DingtalkPlatformAdapter(Platform):
async def terminate(self):
def monkey_patch_close():
raise Exception("Graceful shutdown")
raise KeyboardInterrupt("Graceful shutdown")
self.client_.open_connection = monkey_patch_close
await self.client_.websocket.close(code=1000, reason="Graceful shutdown")

View File

@@ -381,7 +381,9 @@ class TelegramPlatformAdapter(Platform):
f"Telegram document file_path is None, cannot save the file {file_name}.",
)
else:
message.message.append(Comp.File(file=file_path, name=file_name))
message.message.append(
Comp.File(file=file_path, name=file_name, url=file_path)
)
elif update.message.video:
file = await update.message.video.get_file()

View File

@@ -1,7 +1,7 @@
import asyncio
import traceback
from astrbot.core import logger, sp
from astrbot.core import astrbot_config, logger, sp
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
from astrbot.core.db import BaseDatabase
@@ -24,6 +24,7 @@ class ProviderManager:
db_helper: BaseDatabase,
persona_mgr: PersonaManager,
):
self.reload_lock = asyncio.Lock()
self.persona_mgr = persona_mgr
self.acm = acm
config = acm.confs["default"]
@@ -226,6 +227,7 @@ class ProviderManager:
async def load_provider(self, provider_config: dict):
if not provider_config["enable"]:
logger.info(f"Provider {provider_config['id']} is disabled, skipping")
return
if provider_config.get("provider_type", "") == "agent_runner":
return
@@ -434,40 +436,46 @@ class ProviderManager:
)
async def reload(self, provider_config: dict):
await self.terminate_provider(provider_config["id"])
if provider_config["enable"]:
await self.load_provider(provider_config)
async with self.reload_lock:
await self.terminate_provider(provider_config["id"])
if provider_config["enable"]:
await self.load_provider(provider_config)
# 和配置文件保持同步
config_ids = [provider["id"] for provider in self.providers_config]
logger.debug(f"providers in user's config: {config_ids}")
for key in list(self.inst_map.keys()):
if key not in config_ids:
await self.terminate_provider(key)
# 和配置文件保持同步
self.providers_config = astrbot_config["provider"]
config_ids = [provider["id"] for provider in self.providers_config]
logger.info(f"providers in user's config: {config_ids}")
for key in list(self.inst_map.keys()):
if key not in config_ids:
await self.terminate_provider(key)
if len(self.provider_insts) == 0:
self.curr_provider_inst = None
elif self.curr_provider_inst is None and len(self.provider_insts) > 0:
self.curr_provider_inst = self.provider_insts[0]
logger.info(
f"自动选择 {self.curr_provider_inst.meta().id} 作为当前提供商适配器。",
)
if len(self.provider_insts) == 0:
self.curr_provider_inst = None
elif self.curr_provider_inst is None and len(self.provider_insts) > 0:
self.curr_provider_inst = self.provider_insts[0]
logger.info(
f"自动选择 {self.curr_provider_inst.meta().id} 作为当前提供商适配器。",
)
if len(self.stt_provider_insts) == 0:
self.curr_stt_provider_inst = None
elif self.curr_stt_provider_inst is None and len(self.stt_provider_insts) > 0:
self.curr_stt_provider_inst = self.stt_provider_insts[0]
logger.info(
f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。",
)
if len(self.stt_provider_insts) == 0:
self.curr_stt_provider_inst = None
elif (
self.curr_stt_provider_inst is None and len(self.stt_provider_insts) > 0
):
self.curr_stt_provider_inst = self.stt_provider_insts[0]
logger.info(
f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。",
)
if len(self.tts_provider_insts) == 0:
self.curr_tts_provider_inst = None
elif self.curr_tts_provider_inst is None and len(self.tts_provider_insts) > 0:
self.curr_tts_provider_inst = self.tts_provider_insts[0]
logger.info(
f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。",
)
if len(self.tts_provider_insts) == 0:
self.curr_tts_provider_inst = None
elif (
self.curr_tts_provider_inst is None and len(self.tts_provider_insts) > 0
):
self.curr_tts_provider_inst = self.tts_provider_insts[0]
logger.info(
f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。",
)
def get_insts(self):
return self.provider_insts

View File

@@ -1,5 +1,6 @@
import abc
import asyncio
import os
from collections.abc import AsyncGenerator
from astrbot.core.agent.message import Message
@@ -11,6 +12,7 @@ from astrbot.core.provider.entities import (
ToolCallsResult,
)
from astrbot.core.provider.register import provider_cls_map
from astrbot.core.utils.astrbot_path import get_astrbot_path
class AbstractProvider(abc.ABC):
@@ -43,6 +45,14 @@ class AbstractProvider(abc.ABC):
)
return meta
async def test(self):
"""test the provider is a
raises:
Exception: if the provider is not available
"""
...
class Provider(AbstractProvider):
"""Chat Provider"""
@@ -165,6 +175,12 @@ class Provider(AbstractProvider):
return dicts
async def test(self, timeout: float = 45.0):
await asyncio.wait_for(
self.text_chat(prompt="REPLY `PONG` ONLY"),
timeout=timeout,
)
class STTProvider(AbstractProvider):
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -177,6 +193,14 @@ class STTProvider(AbstractProvider):
"""获取音频的文本"""
raise NotImplementedError
async def test(self):
sample_audio_path = os.path.join(
get_astrbot_path(),
"samples",
"stt_health_check.wav",
)
await self.get_text(sample_audio_path)
class TTSProvider(AbstractProvider):
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -189,6 +213,9 @@ class TTSProvider(AbstractProvider):
"""获取文本的音频,返回音频文件路径"""
raise NotImplementedError
async def test(self):
await self.get_audio("hi")
class EmbeddingProvider(AbstractProvider):
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -211,6 +238,9 @@ class EmbeddingProvider(AbstractProvider):
"""获取向量的维度"""
...
async def test(self):
await self.get_embedding("astrbot")
async def get_embeddings_batch(
self,
texts: list[str],
@@ -294,3 +324,8 @@ class RerankProvider(AbstractProvider):
) -> list[RerankResult]:
"""获取查询和文档的重排序分数"""
...
async def test(self):
result = await self.rerank("Apple", documents=["apple", "banana"])
if not result:
raise Exception("Rerank provider test failed, no results returned")

View File

@@ -290,7 +290,7 @@ class ProviderAnthropic(Provider):
try:
llm_response = await self._query(payloads, func_tool)
except Exception as e:
logger.error(f"发生了错误。Provider 配置如下: {model_config}")
# logger.error(f"发生了错误。Provider 配置如下: {model_config}")
raise e
return llm_response

View File

@@ -111,9 +111,9 @@ class ProviderGoogleGenAI(Provider):
f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}...",
)
raise Exception("达到了 Gemini 速率限制, 请稍后再试...")
logger.error(
f"发生了错误(gemini_source)。Provider 配置如下: {self.provider_config}",
)
# logger.error(
# f"发生了错误(gemini_source)。Provider 配置如下: {self.provider_config}",
# )
raise e
async def _prepare_query_config(

View File

@@ -433,7 +433,7 @@ class ProviderOpenAIOfficial(Provider):
)
payloads.pop("tools", None)
return False, chosen_key, available_api_keys, payloads, context_query, None
logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}")
# logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}")
if "tool" in str(e).lower() and "support" in str(e).lower():
logger.error("疑似该模型不支持函数调用工具调用。请输入 /tool off_all")

View File

@@ -171,110 +171,3 @@ class SessionServiceManager:
# 如果没有配置,默认为启用(兼容性考虑)
return True
@staticmethod
def set_session_status(session_id: str, enabled: bool) -> None:
"""设置会话的整体启停状态
Args:
session_id: 会话ID (unified_msg_origin)
enabled: True表示启用False表示禁用
"""
session_config = (
sp.get("session_service_config", {}, scope="umo", scope_id=session_id) or {}
)
session_config["session_enabled"] = enabled
sp.put(
"session_service_config",
session_config,
scope="umo",
scope_id=session_id,
)
logger.info(
f"会话 {session_id} 的整体状态已更新为: {'启用' if enabled else '禁用'}",
)
@staticmethod
def should_process_session_request(event: AstrMessageEvent) -> bool:
"""检查是否应该处理会话请求(会话整体启停检查)
Args:
event: 消息事件
Returns:
bool: True表示应该处理False表示跳过
"""
session_id = event.unified_msg_origin
return SessionServiceManager.is_session_enabled(session_id)
# =============================================================================
# 会话命名相关方法
# =============================================================================
@staticmethod
def get_session_custom_name(session_id: str) -> str | None:
"""获取会话的自定义名称
Args:
session_id: 会话ID (unified_msg_origin)
Returns:
str: 自定义名称如果没有设置则返回None
"""
session_services = sp.get(
"session_service_config",
{},
scope="umo",
scope_id=session_id,
)
return session_services.get("custom_name")
@staticmethod
def set_session_custom_name(session_id: str, custom_name: str) -> None:
"""设置会话的自定义名称
Args:
session_id: 会话ID (unified_msg_origin)
custom_name: 自定义名称,可以为空字符串来清除名称
"""
session_config = (
sp.get("session_service_config", {}, scope="umo", scope_id=session_id) or {}
)
if custom_name and custom_name.strip():
session_config["custom_name"] = custom_name.strip()
else:
# 如果传入空名称,则删除自定义名称
session_config.pop("custom_name", None)
sp.put(
"session_service_config",
session_config,
scope="umo",
scope_id=session_id,
)
logger.info(
f"会话 {session_id} 的自定义名称已更新为: {custom_name.strip() if custom_name and custom_name.strip() else '已清除'}",
)
@staticmethod
def get_session_display_name(session_id: str) -> str:
"""获取会话的显示名称优先显示自定义名称否则显示原始session_id的最后一段
Args:
session_id: 会话ID (unified_msg_origin)
Returns:
str: 显示名称
"""
custom_name = SessionServiceManager.get_session_custom_name(session_id)
if custom_name:
return custom_name
# 如果没有自定义名称返回session_id的最后一段
return session_id.split(":")[2] if session_id.count(":") >= 2 else session_id

View File

@@ -42,87 +42,6 @@ class SessionPluginManager:
# 如果都没有配置,默认为启用(兼容性考虑)
return True
@staticmethod
def set_plugin_status_for_session(
session_id: str,
plugin_name: str,
enabled: bool,
) -> None:
"""设置插件在指定会话中的启停状态
Args:
session_id: 会话ID (unified_msg_origin)
plugin_name: 插件名称
enabled: True表示启用False表示禁用
"""
# 获取当前配置
session_plugin_config = sp.get(
"session_plugin_config",
{},
scope="umo",
scope_id=session_id,
)
if session_id not in session_plugin_config:
session_plugin_config[session_id] = {
"enabled_plugins": [],
"disabled_plugins": [],
}
session_config = session_plugin_config[session_id]
enabled_plugins = session_config.get("enabled_plugins", [])
disabled_plugins = session_config.get("disabled_plugins", [])
if enabled:
# 启用插件
if plugin_name in disabled_plugins:
disabled_plugins.remove(plugin_name)
if plugin_name not in enabled_plugins:
enabled_plugins.append(plugin_name)
else:
# 禁用插件
if plugin_name in enabled_plugins:
enabled_plugins.remove(plugin_name)
if plugin_name not in disabled_plugins:
disabled_plugins.append(plugin_name)
# 保存配置
session_config["enabled_plugins"] = enabled_plugins
session_config["disabled_plugins"] = disabled_plugins
session_plugin_config[session_id] = session_config
sp.put(
"session_plugin_config",
session_plugin_config,
scope="umo",
scope_id=session_id,
)
logger.info(
f"会话 {session_id} 的插件 {plugin_name} 状态已更新为: {'启用' if enabled else '禁用'}",
)
@staticmethod
def get_session_plugin_config(session_id: str) -> dict[str, list[str]]:
"""获取指定会话的插件配置
Args:
session_id: 会话ID (unified_msg_origin)
Returns:
Dict[str, List[str]]: 包含enabled_plugins和disabled_plugins的字典
"""
session_plugin_config = sp.get(
"session_plugin_config",
{},
scope="umo",
scope_id=session_id,
)
return session_plugin_config.get(
session_id,
{"enabled_plugins": [], "disabled_plugins": []},
)
@staticmethod
def filter_handlers_by_session(event: AstrMessageEvent, handlers: list) -> list:
"""根据会话配置过滤处理器列表

View File

@@ -0,0 +1,23 @@
from pathlib import Path
from openai import AsyncOpenAI
async def extract_file_moonshotai(file_path: str, api_key: str) -> str:
"""Extract text from a file using Moonshot AI API"""
"""
Args:
file_path: The path to the file to extract text from
api_key: The API key to use to extract text from the file
Returns:
The text extracted from the file
"""
client = AsyncOpenAI(
api_key=api_key,
base_url="https://api.moonshot.cn/v1",
)
file_object = await client.files.create(
file=Path(file_path),
purpose="file-extract", # type: ignore
)
return (await client.files.content(file_id=file_object.id)).text

View File

@@ -14,14 +14,12 @@ from astrbot.core.config.default import (
DEFAULT_CONFIG,
DEFAULT_VALUE_MAP,
)
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
from astrbot.core.platform.register import platform_cls_map, platform_registry
from astrbot.core.provider import Provider
from astrbot.core.provider.entities import ProviderType
from astrbot.core.provider.provider import RerankProvider
from astrbot.core.provider.register import provider_registry
from astrbot.core.star.star import star_registry
from astrbot.core.utils.astrbot_path import get_astrbot_path
from .route import Response, Route, RouteContext
@@ -133,7 +131,9 @@ def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False)
is_core,
)
else:
errors, post_config = validate_config(post_config, config.schema, is_core)
errors, post_config = validate_config(
post_config, getattr(config, "schema", {}), is_core
)
except BaseException as e:
logger.error(traceback.format_exc())
logger.warning(f"验证配置时出现异常: {e}")
@@ -247,11 +247,8 @@ class ConfigRoute(Route):
async def get_default_config(self):
"""获取默认配置文件"""
return (
Response()
.ok({"config": DEFAULT_CONFIG, "metadata": CONFIG_METADATA_3})
.__dict__
)
metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3)
return Response().ok({"config": DEFAULT_CONFIG, "metadata": metadata}).__dict__
async def get_abconf_list(self):
"""获取所有 AstrBot 配置文件的列表"""
@@ -282,17 +279,15 @@ class ConfigRoute(Route):
try:
if system_config:
abconf = self.acm.confs["default"]
return (
Response()
.ok({"config": abconf, "metadata": CONFIG_METADATA_3_SYSTEM})
.__dict__
metadata = ConfigMetadataI18n.convert_to_i18n_keys(
CONFIG_METADATA_3_SYSTEM
)
return Response().ok({"config": abconf, "metadata": metadata}).__dict__
if abconf_id is None:
raise ValueError("abconf_id cannot be None")
abconf = self.acm.confs[abconf_id]
return (
Response()
.ok({"config": abconf, "metadata": CONFIG_METADATA_3})
.__dict__
)
metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3)
return Response().ok({"config": abconf, "metadata": metadata}).__dict__
except ValueError as e:
return Response().error(str(e)).__dict__
@@ -358,169 +353,20 @@ class ConfigRoute(Route):
f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})",
)
if provider_capability_type == ProviderType.CHAT_COMPLETION:
try:
logger.debug(f"Sending 'Ping' to provider: {status_info['name']}")
response = await asyncio.wait_for(
provider.text_chat(prompt="REPLY `PONG` ONLY"),
timeout=45.0,
)
logger.debug(
f"Received response from {status_info['name']}: {response}",
)
if response is not None:
status_info["status"] = "available"
response_text_snippet = ""
if (
hasattr(response, "completion_text")
and response.completion_text
):
response_text_snippet = (
response.completion_text[:70] + "..."
if len(response.completion_text) > 70
else response.completion_text
)
elif hasattr(response, "result_chain") and response.result_chain:
try:
response_text_snippet = (
response.result_chain.get_plain_text()[:70] + "..."
if len(response.result_chain.get_plain_text()) > 70
else response.result_chain.get_plain_text()
)
except Exception as _:
pass
logger.info(
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'",
)
else:
status_info["error"] = (
"Test call returned None, but expected an LLMResponse object."
)
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.",
)
except asyncio.TimeoutError:
status_info["error"] = (
"Connection timed out after 45 seconds during test call."
)
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.",
)
except Exception as e:
error_message = str(e)
status_info["error"] = error_message
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}",
)
logger.debug(
f"Traceback for {status_info['name']}:\n{traceback.format_exc()}",
)
elif provider_capability_type == ProviderType.EMBEDDING:
try:
# For embedding, we can call the get_embedding method with a short prompt.
embedding_result = await provider.get_embedding("health_check")
if isinstance(embedding_result, list) and (
not embedding_result or isinstance(embedding_result[0], float)
):
status_info["status"] = "available"
else:
status_info["status"] = "unavailable"
status_info["error"] = (
f"Embedding test failed: unexpected result type {type(embedding_result)}"
)
except Exception as e:
logger.error(
f"Error testing embedding provider {provider_name}: {e}",
exc_info=True,
)
status_info["status"] = "unavailable"
status_info["error"] = f"Embedding test failed: {e!s}"
elif provider_capability_type == ProviderType.TEXT_TO_SPEECH:
try:
# For TTS, we can call the get_audio method with a short prompt.
audio_result = await provider.get_audio("你好")
if isinstance(audio_result, str) and audio_result:
status_info["status"] = "available"
else:
status_info["status"] = "unavailable"
status_info["error"] = (
f"TTS test failed: unexpected result type {type(audio_result)}"
)
except Exception as e:
logger.error(
f"Error testing TTS provider {provider_name}: {e}",
exc_info=True,
)
status_info["status"] = "unavailable"
status_info["error"] = f"TTS test failed: {e!s}"
elif provider_capability_type == ProviderType.SPEECH_TO_TEXT:
try:
logger.debug(
f"Sending health check audio to provider: {status_info['name']}",
)
sample_audio_path = os.path.join(
get_astrbot_path(),
"samples",
"stt_health_check.wav",
)
if not os.path.exists(sample_audio_path):
status_info["status"] = "unavailable"
status_info["error"] = (
"STT test failed: sample audio file not found."
)
logger.warning(
f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}",
)
else:
text_result = await provider.get_text(sample_audio_path)
if isinstance(text_result, str) and text_result:
status_info["status"] = "available"
snippet = (
text_result[:70] + "..."
if len(text_result) > 70
else text_result
)
logger.info(
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'",
)
else:
status_info["status"] = "unavailable"
status_info["error"] = (
f"STT test failed: unexpected result type {type(text_result)}"
)
logger.warning(
f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}",
)
except Exception as e:
logger.error(
f"Error testing STT provider {provider_name}: {e}",
exc_info=True,
)
status_info["status"] = "unavailable"
status_info["error"] = f"STT test failed: {e!s}"
elif provider_capability_type == ProviderType.RERANK:
try:
assert isinstance(provider, RerankProvider)
await provider.rerank("Apple", documents=["apple", "banana"])
status_info["status"] = "available"
except Exception as e:
logger.error(
f"Error testing rerank provider {provider_name}: {e}",
exc_info=True,
)
status_info["status"] = "unavailable"
status_info["error"] = f"Rerank test failed: {e!s}"
else:
logger.debug(
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}",
)
try:
await provider.test()
status_info["status"] = "available"
status_info["error"] = (
"This provider type is not tested and is assumed to be available."
logger.info(
f"Provider {status_info['name']} (ID: {status_info['id']}) is available.",
)
except Exception as e:
error_message = str(e)
status_info["error"] = error_message
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}",
)
logger.debug(
f"Traceback for {status_info['name']}:\n{traceback.format_exc()}",
)
return status_info
@@ -598,9 +444,15 @@ class ConfigRoute(Route):
return Response().error("缺少参数 provider_id").__dict__
prov_mgr = self.core_lifecycle.provider_manager
provider: Provider | None = prov_mgr.inst_map.get(provider_id, None)
provider = prov_mgr.inst_map.get(provider_id, None)
if not provider:
return Response().error(f"未找到 ID 为 {provider_id} 的提供商").__dict__
if not isinstance(provider, Provider):
return (
Response()
.error(f"提供商 {provider_id} 类型不支持获取模型列表")
.__dict__
)
try:
models = await provider.get_models()

View File

@@ -60,10 +60,6 @@ class KnowledgeBaseRoute(Route):
# "/kb/media/delete": ("POST", self.delete_media),
# 检索
"/kb/retrieve": ("POST", self.retrieve),
# 会话知识库配置
"/kb/session/config/get": ("GET", self.get_session_kb_config),
"/kb/session/config/set": ("POST", self.set_session_kb_config),
"/kb/session/config/delete": ("POST", self.delete_session_kb_config),
}
self.register_routes()
@@ -920,158 +916,6 @@ class KnowledgeBaseRoute(Route):
logger.error(traceback.format_exc())
return Response().error(f"检索失败: {e!s}").__dict__
# ===== 会话知识库配置 API =====
async def get_session_kb_config(self):
"""获取会话的知识库配置
Query 参数:
- session_id: 会话 ID (必填)
返回:
- kb_ids: 知识库 ID 列表
- top_k: 返回结果数量
- enable_rerank: 是否启用重排序
"""
try:
from astrbot.core import sp
session_id = request.args.get("session_id")
if not session_id:
return Response().error("缺少参数 session_id").__dict__
# 从 SharedPreferences 获取配置
config = await sp.session_get(session_id, "kb_config", default={})
logger.debug(f"[KB配置] 读取到配置: session_id={session_id}")
# 如果没有配置,返回默认值
if not config:
config = {"kb_ids": [], "top_k": 5, "enable_rerank": True}
return Response().ok(config).__dict__
except Exception as e:
logger.error(f"[KB配置] 获取配置时出错: {e}", exc_info=True)
return Response().error(f"获取会话知识库配置失败: {e!s}").__dict__
async def set_session_kb_config(self):
"""设置会话的知识库配置
Body:
- scope: 配置范围 (目前只支持 "session")
- scope_id: 会话 ID (必填)
- kb_ids: 知识库 ID 列表 (必填)
- top_k: 返回结果数量 (可选, 默认 5)
- enable_rerank: 是否启用重排序 (可选, 默认 true)
"""
try:
from astrbot.core import sp
data = await request.json
scope = data.get("scope")
scope_id = data.get("scope_id")
kb_ids = data.get("kb_ids", [])
top_k = data.get("top_k", 5)
enable_rerank = data.get("enable_rerank", True)
# 验证参数
if scope != "session":
return Response().error("目前仅支持 session 范围的配置").__dict__
if not scope_id:
return Response().error("缺少参数 scope_id").__dict__
if not isinstance(kb_ids, list):
return Response().error("kb_ids 必须是列表").__dict__
# 验证知识库是否存在
kb_mgr = self._get_kb_manager()
invalid_ids = []
valid_ids = []
for kb_id in kb_ids:
kb_helper = await kb_mgr.get_kb(kb_id)
if kb_helper:
valid_ids.append(kb_id)
else:
invalid_ids.append(kb_id)
logger.warning(f"[KB配置] 知识库不存在: {kb_id}")
if invalid_ids:
logger.warning(f"[KB配置] 以下知识库ID无效: {invalid_ids}")
# 允许保存空列表,表示明确不使用任何知识库
if kb_ids and not valid_ids:
# 只有当用户提供了 kb_ids 但全部无效时才报错
return Response().error(f"所有提供的知识库ID都无效: {kb_ids}").__dict__
# 如果 kb_ids 为空列表,表示用户想清空配置
if not kb_ids:
valid_ids = []
# 构建配置对象只保存有效的ID
config = {
"kb_ids": valid_ids,
"top_k": top_k,
"enable_rerank": enable_rerank,
}
# 保存到 SharedPreferences
await sp.session_put(scope_id, "kb_config", config)
# 立即验证是否保存成功
verify_config = await sp.session_get(scope_id, "kb_config", default={})
if verify_config == config:
return (
Response()
.ok(
{"valid_ids": valid_ids, "invalid_ids": invalid_ids},
"保存知识库配置成功",
)
.__dict__
)
logger.error("[KB配置] 配置保存失败,验证不匹配")
return Response().error("配置保存失败").__dict__
except Exception as e:
logger.error(f"[KB配置] 设置配置时出错: {e}", exc_info=True)
return Response().error(f"设置会话知识库配置失败: {e!s}").__dict__
async def delete_session_kb_config(self):
"""删除会话的知识库配置
Body:
- scope: 配置范围 (目前只支持 "session")
- scope_id: 会话 ID (必填)
"""
try:
from astrbot.core import sp
data = await request.json
scope = data.get("scope")
scope_id = data.get("scope_id")
# 验证参数
if scope != "session":
return Response().error("目前仅支持 session 范围的配置").__dict__
if not scope_id:
return Response().error("缺少参数 scope_id").__dict__
# 从 SharedPreferences 删除配置
await sp.session_remove(scope_id, "kb_config")
return Response().ok(message="删除知识库配置成功").__dict__
except Exception as e:
logger.error(f"删除会话知识库配置失败: {e}")
logger.error(traceback.format_exc())
return Response().error(f"删除会话知识库配置失败: {e!s}").__dict__
async def upload_document_from_url(self):
"""从 URL 上传文档

View File

@@ -1,3 +1,4 @@
import asyncio
import json
import os
import ssl
@@ -19,6 +20,10 @@ from astrbot.core.star.star_manager import PluginManager
from .route import Response, Route, RouteContext
PLUGIN_UPDATE_CONCURRENCY = (
3 # limit concurrent updates to avoid overwhelming plugin sources
)
class PluginRoute(Route):
def __init__(
@@ -33,6 +38,7 @@ class PluginRoute(Route):
"/plugin/install": ("POST", self.install_plugin),
"/plugin/install-upload": ("POST", self.install_plugin_upload),
"/plugin/update": ("POST", self.update_plugin),
"/plugin/update-all": ("POST", self.update_all_plugins),
"/plugin/uninstall": ("POST", self.uninstall_plugin),
"/plugin/market_list": ("GET", self.get_online_plugins),
"/plugin/off": ("POST", self.off_plugin),
@@ -63,7 +69,7 @@ class PluginRoute(Route):
.__dict__
)
data = await request.json
data = await request.get_json()
plugin_name = data.get("name", None)
try:
success, message = await self.plugin_manager.reload(plugin_name)
@@ -346,7 +352,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
repo_url = post_data["url"]
proxy: str = post_data.get("proxy", None)
@@ -393,7 +399,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
delete_config = post_data.get("delete_config", False)
delete_data = post_data.get("delete_data", False)
@@ -418,7 +424,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
proxy: str = post_data.get("proxy", None)
try:
@@ -432,6 +438,59 @@ class PluginRoute(Route):
logger.error(f"/api/plugin/update: {traceback.format_exc()}")
return Response().error(str(e)).__dict__
async def update_all_plugins(self):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode")
.__dict__
)
post_data = await request.get_json()
plugin_names: list[str] = post_data.get("names") or []
proxy: str = post_data.get("proxy", "")
if not isinstance(plugin_names, list) or not plugin_names:
return Response().error("插件列表不能为空").__dict__
results = []
sem = asyncio.Semaphore(PLUGIN_UPDATE_CONCURRENCY)
async def _update_one(name: str):
async with sem:
try:
logger.info(f"批量更新插件 {name}")
await self.plugin_manager.update_plugin(name, proxy)
return {"name": name, "status": "ok", "message": "更新成功"}
except Exception as e:
logger.error(
f"/api/plugin/update-all: 更新插件 {name} 失败: {traceback.format_exc()}",
)
return {"name": name, "status": "error", "message": str(e)}
raw_results = await asyncio.gather(
*(_update_one(name) for name in plugin_names),
return_exceptions=True,
)
for name, result in zip(plugin_names, raw_results):
if isinstance(result, asyncio.CancelledError):
raise result
if isinstance(result, BaseException):
results.append(
{"name": name, "status": "error", "message": str(result)}
)
else:
results.append(result)
failed = [r for r in results if r["status"] == "error"]
message = (
"批量更新完成,全部成功。"
if not failed
else f"批量更新完成,其中 {len(failed)}/{len(results)} 个插件失败。"
)
return Response().ok({"results": results}, message).__dict__
async def off_plugin(self):
if DEMO_MODE:
return (
@@ -440,7 +499,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
try:
await self.plugin_manager.turn_off_plugin(plugin_name)
@@ -458,7 +517,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
try:
await self.plugin_manager.turn_on_plugin(plugin_name)

File diff suppressed because it is too large Load Diff

18
changelogs/v4.7.0.md Normal file
View File

@@ -0,0 +1,18 @@
## What's Changed
重构:
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
优化:
- Dify、阿里云百炼应用支持流式输出
- 防止分段回复正则表达式解析错误导致消息不发送
- 群聊上下文感知记录 At 信息
- 优化模型提供商页面的测试提供商功能
新增:
- 支持在配置文件页面快速测试对话
- 为配置文件配置项内容添加国际化支持
修复:
- 在更新 MCP Server 配置后MCP 无法正常重启的问题

22
changelogs/v4.7.1.md Normal file
View File

@@ -0,0 +1,22 @@
## What's Changed
### 修复了自定义规则页面无法设置插件和知识库的规则的问题
---
重构:
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界。详见:[Agent 执行器](https://docs.astrbot.app/use/agent-runner.html)
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
优化:
- Dify、阿里云百炼应用支持流式输出
- 防止分段回复正则表达式解析错误导致消息不发送
- 群聊上下文感知记录 At 信息
- 优化模型提供商页面的测试提供商功能
新增:
- 支持在配置文件页面快速测试对话
- 为配置文件配置项内容添加国际化支持
修复:
- 在更新 MCP Server 配置后MCP 无法正常重启的问题

25
changelogs/v4.7.3.md Normal file
View File

@@ -0,0 +1,25 @@
## What's Changed
1. 修复使用非默认配置文件情况下时,第三方 Agent Runner (Dify、Coze、阿里云百炼应用等)无法正常工作的问题
2. 修复当“聊天模型”未设置,并且模型提供商中仅有 Agent Runner 时,无法正常使用 Agent Runner 的问题
3. 修复部分情况下报错 `pydantic_core._pydantic_core.ValidationError: 1 validation error for Message content` 的问题
4. 新增群聊模式下的专用图片转述模型配置 ([#3822](https://github.com/AstrBotDevs/AstrBot/issues/3822))
---
重构:
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界。详见:[Agent 执行器](https://docs.astrbot.app/use/agent-runner.html)
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
优化:
- Dify、阿里云百炼应用支持流式输出
- 防止分段回复正则表达式解析错误导致消息不发送
- 群聊上下文感知记录 At 信息
- 优化模型提供商页面的测试提供商功能
新增:
- 支持在配置文件页面快速测试对话
- 为配置文件配置项内容添加国际化支持
修复:
- 在更新 MCP Server 配置后MCP 无法正常重启的问题

7
changelogs/v4.7.4.md Normal file
View File

@@ -0,0 +1,7 @@
## What's Changed
1. 修复assistant message 中 tool_call 存在但 content 不存在时,导致验证错误的问题 ([#3862](https://github.com/AstrBotDevs/AstrBot/issues/3862))
2. 修复fix: aiocqhttp 适配器 NapCat 文件名获取为空 ([#3853](https://github.com/AstrBotDevs/AstrBot/issues/3853))
3. 新增:升级所有插件按钮
4. 新增:/provider 指令支持同时测试提供商可用性
5. 优化:主动回复的 prompt

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -84,7 +84,7 @@
v-model:prompt="prompt"
:stagedImagesUrl="stagedImagesUrl"
:stagedAudioUrl="stagedAudioUrl"
:disabled="isStreaming || isConvRunning"
:disabled="isStreaming"
:enableStreaming="enableStreaming"
:isRecording="isRecording"
:session-id="currSessionId || null"

View File

@@ -15,6 +15,7 @@
:session-id="sessionId || null"
:platform-id="sessionPlatformId"
:is-group="sessionIsGroup"
:initial-config-id="props.configId"
@config-changed="handleConfigChange"
/>
<ProviderModelSelector v-if="showProviderSelector" ref="providerModelSelectorRef" />
@@ -79,11 +80,13 @@ interface Props {
isRecording: boolean;
sessionId?: string | null;
currentSession?: Session | null;
configId?: string | null;
}
const props = withDefaults(defineProps<Props>(), {
sessionId: null,
currentSession: null
currentSession: null,
configId: null
});
const emit = defineEmits<{

View File

@@ -90,10 +90,12 @@ const props = withDefaults(defineProps<{
sessionId?: string | null;
platformId?: string;
isGroup?: boolean;
initialConfigId?: string | null;
}>(), {
sessionId: null,
platformId: 'webchat',
isGroup: false
isGroup: false,
initialConfigId: null
});
const emit = defineEmits<{ 'config-changed': [ConfigChangedPayload] }>();
@@ -291,7 +293,7 @@ watch(
onMounted(async () => {
await fetchConfigList();
const stored = localStorage.getItem(STORAGE_KEY) || 'default';
const stored = props.initialConfigId || localStorage.getItem(STORAGE_KEY) || 'default';
selectedConfigId.value = stored;
await setSelection(stored);
await syncSelectionForSession();

View File

@@ -549,7 +549,7 @@ export default {
}
.bot-embedded-image {
max-width: 80%;
max-width: 40%;
width: auto;
height: auto;
border-radius: 8px;
@@ -558,10 +558,6 @@ export default {
transition: transform 0.2s ease;
}
.bot-embedded-image:hover {
transform: scale(1.02);
}
.embedded-audio {
width: 300px;
margin-top: 8px;

View File

@@ -0,0 +1,319 @@
<template>
<v-card class="standalone-chat-card" elevation="0" rounded="0">
<v-card-text class="standalone-chat-container">
<div class="chat-layout">
<!-- 聊天内容区域 -->
<div class="chat-content-panel">
<MessageList v-if="messages && messages.length > 0" :messages="messages" :isDark="isDark"
:isStreaming="isStreaming || isConvRunning" @openImagePreview="openImagePreview"
ref="messageList" />
<div class="welcome-container fade-in" v-else>
<div class="welcome-title">
<span>Hello, I'm</span>
<span class="bot-name">AstrBot ⭐</span>
</div>
<p class="text-caption text-medium-emphasis mt-2">
测试配置: {{ configId || 'default' }}
</p>
</div>
<!-- 输入区域 -->
<ChatInput
v-model:prompt="prompt"
:stagedImagesUrl="stagedImagesUrl"
:stagedAudioUrl="stagedAudioUrl"
:disabled="isStreaming"
:enableStreaming="enableStreaming"
:isRecording="isRecording"
:session-id="currSessionId || null"
:current-session="getCurrentSession"
:config-id="configId"
@send="handleSendMessage"
@toggleStreaming="toggleStreaming"
@removeImage="removeImage"
@removeAudio="removeAudio"
@startRecording="handleStartRecording"
@stopRecording="handleStopRecording"
@pasteImage="handlePaste"
@fileSelect="handleFileSelect"
ref="chatInputRef"
/>
</div>
</div>
</v-card-text>
</v-card>
<!-- 图片预览对话框 -->
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
<v-card class="image-preview-card" elevation="8">
<v-card-title class="d-flex justify-space-between align-center pa-4">
<span>{{ t('core.common.imagePreview') }}</span>
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
</v-card-title>
<v-card-text class="text-center pa-4">
<img :src="previewImageUrl" class="preview-image-large" />
</v-card-text>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
import axios from 'axios';
import { useCustomizerStore } from '@/stores/customizer';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { useTheme } from 'vuetify';
import MessageList from '@/components/chat/MessageList.vue';
import ChatInput from '@/components/chat/ChatInput.vue';
import { useMessages } from '@/composables/useMessages';
import { useMediaHandling } from '@/composables/useMediaHandling';
import { useRecording } from '@/composables/useRecording';
import { useToast } from '@/utils/toast';
interface Props {
configId?: string | null;
}
const props = withDefaults(defineProps<Props>(), {
configId: null
});
const { t } = useI18n();
const { error: showError } = useToast();
// UI 状态
const imagePreviewDialog = ref(false);
const previewImageUrl = ref('');
// 会话管理(不使用 useSessions 避免路由跳转)
const currSessionId = ref('');
const getCurrentSession = computed(() => null); // 独立测试模式不需要会话信息
async function newSession() {
try {
const response = await axios.get('/api/chat/new_session');
const sessionId = response.data.data.session_id;
currSessionId.value = sessionId;
return sessionId;
} catch (err) {
console.error(err);
throw err;
}
}
function updateSessionTitle(sessionId: string, title: string) {
// 独立模式不需要更新会话标题
}
function getSessions() {
// 独立模式不需要加载会话列表
}
const {
stagedImagesName,
stagedImagesUrl,
stagedAudioUrl,
getMediaFile,
processAndUploadImage,
handlePaste,
removeImage,
removeAudio,
clearStaged,
cleanupMediaCache
} = useMediaHandling();
const { isRecording, startRecording: startRec, stopRecording: stopRec } = useRecording();
const {
messages,
isStreaming,
isConvRunning,
enableStreaming,
getSessionMessages: getSessionMsg,
sendMessage: sendMsg,
toggleStreaming
} = useMessages(currSessionId, getMediaFile, updateSessionTitle, getSessions);
// 组件引用
const messageList = ref<InstanceType<typeof MessageList> | null>(null);
const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
// 输入状态
const prompt = ref('');
const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark');
function openImagePreview(imageUrl: string) {
previewImageUrl.value = imageUrl;
imagePreviewDialog.value = true;
}
async function handleStartRecording() {
await startRec();
}
async function handleStopRecording() {
const audioFilename = await stopRec();
stagedAudioUrl.value = audioFilename;
}
async function handleFileSelect(files: FileList) {
for (const file of files) {
await processAndUploadImage(file);
}
}
async function handleSendMessage() {
if (!prompt.value.trim() && stagedImagesName.value.length === 0 && !stagedAudioUrl.value) {
return;
}
try {
if (!currSessionId.value) {
await newSession();
}
const promptToSend = prompt.value.trim();
const imageNamesToSend = [...stagedImagesName.value];
const audioNameToSend = stagedAudioUrl.value;
// 清空输入和附件
prompt.value = '';
clearStaged();
// 获取选择的提供商和模型
const selection = chatInputRef.value?.getCurrentSelection();
const selectedProviderId = selection?.providerId || '';
const selectedModelName = selection?.modelName || '';
await sendMsg(
promptToSend,
imageNamesToSend,
audioNameToSend,
selectedProviderId,
selectedModelName
);
// 滚动到底部
nextTick(() => {
messageList.value?.scrollToBottom();
});
} catch (err) {
console.error('Failed to send message:', err);
showError(t('features.chat.errors.sendMessageFailed'));
// 恢复输入内容,让用户可以重试
// 注意:附件已经上传到服务器,所以不恢复附件
}
}
onMounted(async () => {
// 独立模式在挂载时创建新会话
try {
await newSession();
} catch (err) {
console.error('Failed to create initial session:', err);
showError(t('features.chat.errors.createSessionFailed'));
}
});
onBeforeUnmount(() => {
cleanupMediaCache();
});
</script>
<style scoped>
/* 基础动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.standalone-chat-card {
width: 100%;
height: 100%;
max-height: 100%;
overflow: hidden;
}
.standalone-chat-container {
width: 100%;
height: 100%;
max-height: 100%;
padding: 0;
overflow: hidden;
}
.chat-layout {
height: 100%;
max-height: 100%;
display: flex;
overflow: hidden;
}
.chat-content-panel {
height: 100%;
max-height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
padding-left: 16px;
border-bottom: 1px solid var(--v-theme-border);
width: 100%;
padding-right: 32px;
flex-shrink: 0;
}
.conversation-header-info h4 {
margin: 0;
font-weight: 500;
}
.conversation-header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.welcome-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.welcome-title {
font-size: 28px;
margin-bottom: 8px;
}
.bot-name {
font-weight: 700;
margin-left: 8px;
color: var(--v-theme-secondary);
}
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
.preview-image-large {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
</style>

View File

@@ -4,7 +4,7 @@
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
style="font-weight: 1000; font-size: 15px">
{{ metadata[key]['name'] }}
{{ tm(metadata[key]['name']) }}
</v-tab>
</v-tabs>
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
@@ -59,7 +59,17 @@ export default {
}
},
setup() {
const { tm } = useModuleI18n('features/config');
const { tm: tmConfig } = useModuleI18n('features/config');
const { tm: tmMetadata } = useModuleI18n('features/config-metadata');
const tm = (key) => {
const metadataResult = tmMetadata(key);
if (!metadataResult.startsWith('[MISSING:') && !metadataResult.startsWith('[INVALID:')) {
return metadataResult;
}
return tmConfig(key);
};
return {
tm
};

View File

@@ -58,7 +58,7 @@
</v-col>
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
<v-alert type="info" variant="tonal">
{{ t('dialogs.addProvider.noTemplates') }}
{{ tm('dialogs.addProvider.noTemplates') }}
</v-alert>
</v-col>
</v-row>

View File

@@ -8,7 +8,7 @@ import PersonaSelector from './PersonaSelector.vue'
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
import PluginSetSelector from './PluginSetSelector.vue'
import T2ITemplateEditor from './T2ITemplateEditor.vue'
import { useI18n } from '@/i18n/composables'
import { useI18n, useModuleI18n } from '@/i18n/composables'
const props = defineProps({
@@ -27,6 +27,34 @@ const props = defineProps({
})
const { t } = useI18n()
const { tm } = useModuleI18n('features/config-metadata')
// 翻译器函数 - 如果是国际化键则翻译,否则原样返回
const translateIfKey = (value) => {
if (!value || typeof value !== 'string') return value
return tm(value)
}
// 处理labels翻译 - labels可以是数组或国际化键
const getTranslatedLabels = (itemMeta) => {
if (!itemMeta?.labels) return null
// 如果labels是字符串国际化键
if (typeof itemMeta.labels === 'string') {
const translatedLabels = tm(itemMeta.labels)
// 如果翻译成功且是数组,返回翻译结果
if (Array.isArray(translatedLabels)) {
return translatedLabels
}
}
// 如果labels是数组直接返回
if (Array.isArray(itemMeta.labels)) {
return itemMeta.labels
}
return null
}
const dialog = ref(false)
const currentEditingKey = ref('')
@@ -158,11 +186,11 @@ function getSpecialSubtype(value) {
rounded="md" variant="outlined">
<v-card-text class="config-section" v-if="metadata[metadataKey]?.type === 'object'" style="padding-bottom: 8px;">
<v-list-item-title class="config-title">
{{ metadata[metadataKey]?.description }}
{{ translateIfKey(metadata[metadataKey]?.description) }}
</v-list-item-title>
<v-list-item-subtitle class="config-hint">
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint"></span>
{{ metadata[metadataKey]?.hint }}
{{ translateIfKey(metadata[metadataKey]?.hint) }}
</v-list-item-subtitle>
</v-card-text>
@@ -176,13 +204,13 @@ function getSpecialSubtype(value) {
<v-col cols="12" sm="6" class="property-info">
<v-list-item density="compact">
<v-list-item-title class="property-name">
{{ itemMeta?.description || itemKey }}
{{ translateIfKey(itemMeta?.description) || itemKey }}
<span class="property-key">({{ itemKey }})</span>
</v-list-item-title>
<v-list-item-subtitle class="property-hint">
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint"></span>
{{ itemMeta?.hint }}
{{ translateIfKey(itemMeta?.hint) }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
@@ -190,7 +218,12 @@ function getSpecialSubtype(value) {
<div class="w-100" v-if="!itemMeta?._special">
<!-- Select input for JSON selector -->
<v-select v-if="itemMeta?.options" v-model="createSelectorModel(itemKey).value"
:items="itemMeta?.labels ? itemMeta.options.map((value, index) => ({ title: itemMeta.labels[index] || value, value: value })) : itemMeta.options"
:items="(() => {
const labels = getTranslatedLabels(itemMeta);
return labels
? itemMeta.options.map((value, index) => ({ title: labels[index] || value, value: value }))
: itemMeta.options;
})()"
:disabled="itemMeta?.readonly" density="compact" variant="outlined"
class="config-field" hide-details></v-select>

View File

@@ -7,8 +7,8 @@ import { useCommonStore } from '@/stores/common';
<!-- 添加筛选级别控件 -->
<div class="filter-controls mb-2" v-if="showLevelBtns">
<v-chip-group v-model="selectedLevels" column multiple>
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'">
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter variant="flat" size="small"
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'" class="font-weight-medium">
{{ level }}
</v-chip>
</v-chip-group>
@@ -168,6 +168,7 @@ export default {
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
margin-left: 20px;
}
.fade-in {

View File

@@ -3,7 +3,7 @@
<div style="flex: 1; min-width: 0; overflow: hidden;">
<span v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
style="color: rgb(var(--v-theme-primaryText));">
未选择
{{ tm('knowledgeBaseSelector.notSelected') }}
</span>
<div v-else class="d-flex flex-wrap gap-1">
<v-chip
@@ -28,7 +28,7 @@
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
选择知识库
{{ tm('knowledgeBaseSelector.dialogTitle') }}
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
@@ -50,9 +50,9 @@
</template>
<v-list-item-title>{{ kb.kb_name }}</v-list-item-title>
<v-list-item-subtitle>
{{ kb.description || '无描述' }}
<span v-if="kb.doc_count !== undefined"> - {{ kb.doc_count }} 个文档</span>
<span v-if="kb.chunk_count !== undefined"> - {{ kb.chunk_count }} 个块</span>
{{ kb.description || tm('knowledgeBaseSelector.noDescription') }}
<span v-if="kb.doc_count !== undefined"> - {{ tm('knowledgeBaseSelector.documentCount', { count: kb.doc_count }) }}</span>
<span v-if="kb.chunk_count !== undefined"> - {{ tm('knowledgeBaseSelector.chunkCount', { count: kb.chunk_count }) }}</span>
</v-list-item-subtitle>
<template v-slot:append>
@@ -68,9 +68,9 @@
<!-- 当没有知识库时显示创建提示 -->
<div v-if="knowledgeBaseList.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-database-off</v-icon>
<p class="text-grey mt-4 mb-4">暂无知识库</p>
<p class="text-grey mt-4 mb-4">{{ tm('knowledgeBaseSelector.noKnowledgeBases') }}</p>
<v-btn color="primary" variant="tonal" @click="goToKnowledgeBasePage">
创建知识库
{{ tm('knowledgeBaseSelector.createKnowledgeBase') }}
</v-btn>
</div>
</v-list>
@@ -78,14 +78,14 @@
<v-card-actions class="pa-4">
<div v-if="selectedKnowledgeBases.length > 0" class="text-caption text-grey">
已选择 {{ selectedKnowledgeBases.length }} 个知识库
{{ tm('knowledgeBaseSelector.selectedCount', { count: selectedKnowledgeBases.length }) }}
</div>
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
<v-btn variant="text" @click="cancelSelection">{{ tm('knowledgeBaseSelector.cancelSelection') }}</v-btn>
<v-btn
color="primary"
@click="confirmSelection">
确认选择
{{ tm('knowledgeBaseSelector.confirmSelection') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -96,6 +96,7 @@
import { ref, watch } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import { useModuleI18n } from '@/i18n/composables'
const props = defineProps({
modelValue: {
@@ -110,6 +111,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const router = useRouter()
const { tm } = useModuleI18n('core.shared')
const dialog = ref(false)
const knowledgeBaseList = ref([])

View File

@@ -4,13 +4,13 @@
<div class="d-flex align-center justify-space-between mb-2">
<div class="flex-grow-1">
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
未启用任何插件
{{ tm('pluginSetSelector.notSelected') }}
</span>
<span v-else-if="isAllPlugins" style="color: rgb(var(--v-theme-primaryText));">
启用所有插件 (*)
{{ tm('pluginSetSelector.allPlugins') }}
</span>
<span v-else style="color: rgb(var(--v-theme-primaryText));">
已选择 {{ modelValue.length }} 个插件
{{ tm('pluginSetSelector.selectedCount', { count: modelValue.length }) }}
</span>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
@@ -23,7 +23,7 @@
<v-dialog v-model="dialog" max-width="700px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
选择插件集合
{{ tm('pluginSetSelector.dialogTitle') }}
</v-card-title>
<v-card-text class="pa-4">
@@ -34,17 +34,17 @@
<v-radio-group v-model="selectionMode" class="mb-4" hide-details>
<v-radio
value="all"
label="启用所有插件"
:label="tm('pluginSetSelector.enableAll')"
color="primary"
></v-radio>
<v-radio
value="none"
label="不启用任何插件"
:label="tm('pluginSetSelector.enableNone')"
color="primary"
></v-radio>
<v-radio
value="custom"
label="自定义选择"
:label="tm('pluginSetSelector.customSelect')"
color="primary"
></v-radio>
</v-radio-group>
@@ -68,21 +68,21 @@
<v-list-item-title>{{ plugin.name }}</v-list-item-title>
<v-list-item-subtitle>
{{ plugin.desc || '无描述' }}
{{ plugin.desc || tm('pluginSetSelector.noDescription') }}
<v-chip v-if="!plugin.activated" size="x-small" color="grey" class="ml-1">
未激活
{{ tm('pluginSetSelector.notActivated') }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<div class="pl-8 pt-2">
<small>*不显示系统插件和已经在插件页禁用的插件</small>
<small>{{ tm('pluginSetSelector.note') }}</small>
</div>
</v-list>
<div v-else class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-puzzle-outline</v-icon>
<p class="text-grey mt-4">暂无可用的插件</p>
<p class="text-grey mt-4">{{ tm('pluginSetSelector.noPlugins') }}</p>
</div>
</div>
</div>
@@ -90,11 +90,11 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
<v-btn variant="text" @click="cancelSelection">{{ tm('pluginSetSelector.cancelSelection') }}</v-btn>
<v-btn
color="primary"
@click="confirmSelection">
确认选择
{{ tm('pluginSetSelector.confirmSelection') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -104,6 +104,7 @@
<script setup>
import { ref, computed, watch } from 'vue'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
const props = defineProps({
modelValue: {
@@ -121,6 +122,7 @@ const props = defineProps({
})
const emit = defineEmits(['update:modelValue'])
const { tm } = useModuleI18n('core.shared')
const dialog = ref(false)
const pluginList = ref([])

View File

@@ -1,7 +1,7 @@
<template>
<div class="d-flex align-center justify-space-between">
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
未选择
{{ tm('providerSelector.notSelected') }}
</span>
<span v-else>
{{ modelValue }}
@@ -15,7 +15,7 @@
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
选择提供商
{{ tm('providerSelector.dialogTitle') }}
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
@@ -30,8 +30,8 @@
:active="selectedProvider === ''"
rounded="md"
class="ma-1">
<v-list-item-title>不选择</v-list-item-title>
<v-list-item-subtitle>清除当前选择</v-list-item-subtitle>
<v-list-item-title>{{ tm('providerSelector.clearSelection') }}</v-list-item-title>
<v-list-item-subtitle>{{ tm('providerSelector.clearSelectionSubtitle') }}</v-list-item-subtitle>
<template v-slot:append>
<v-icon v-if="selectedProvider === ''" color="primary">mdi-check-circle</v-icon>
@@ -50,7 +50,7 @@
class="ma-1">
<v-list-item-title>{{ provider.id }}</v-list-item-title>
<v-list-item-subtitle>
{{ provider.type || provider.provider_type || '未知类型' }}
{{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }}
<span v-if="provider.model_config?.model">- {{ provider.model_config.model }}</span>
</v-list-item-subtitle>
@@ -62,7 +62,7 @@
<div v-else-if="!loading && providerList.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
<p class="text-grey mt-4">暂无可用的提供商</p>
<p class="text-grey mt-4">{{ tm('providerSelector.noProviders') }}</p>
</div>
</v-card-text>
@@ -70,11 +70,11 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
<v-btn variant="text" @click="cancelSelection">{{ tm('providerSelector.cancelSelection') }}</v-btn>
<v-btn
color="primary"
@click="confirmSelection">
确认选择
{{ tm('providerSelector.confirmSelection') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -84,6 +84,7 @@
<script setup>
import { ref, watch } from 'vue'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
const props = defineProps({
modelValue: {
@@ -105,6 +106,7 @@ const props = defineProps({
})
const emit = defineEmits(['update:modelValue'])
const { tm } = useModuleI18n('core.shared')
const dialog = ref(false)
const providerList = ref([])

View File

@@ -33,6 +33,7 @@ export class I18nLoader {
{ name: 'core/status', path: 'core/status.json' },
{ name: 'core/navigation', path: 'core/navigation.json' },
{ name: 'core/header', path: 'core/header.json' },
{ name: 'core/shared', path: 'core/shared.json' },
// 功能模块
{ name: 'features/chat', path: 'features/chat.json' },
@@ -43,6 +44,7 @@ export class I18nLoader {
{ name: 'features/provider', path: 'features/provider.json' },
{ name: 'features/platform', path: 'features/platform.json' },
{ name: 'features/config', path: 'features/config.json' },
{ name: 'features/config-metadata', path: 'features/config-metadata.json' },
{ name: 'features/console', path: 'features/console.json' },
{ name: 'features/about', path: 'features/about.json' },
{ name: 'features/settings', path: 'features/settings.json' },

View File

@@ -8,7 +8,7 @@
"chat": "Chat",
"extension": "Extensions",
"conversation": "Conversations",
"sessionManagement": "Session Management",
"sessionManagement": "Custom Rules",
"console": "Console",
"alkaid": "Alkaid Lab",
"knowledgeBase": "Knowledge Base",

View File

@@ -0,0 +1,45 @@
{
"knowledgeBaseSelector": {
"notSelected": "Not selected",
"buttonText": "Select Knowledge Base...",
"dialogTitle": "Select Knowledge Base",
"loading": "Loading...",
"noKnowledgeBases": "No knowledge bases available",
"createKnowledgeBase": "Create Knowledge Base",
"selectedCount": "{count} knowledge base(s) selected",
"confirmSelection": "Confirm Selection",
"cancelSelection": "Cancel",
"noDescription": "No description",
"documentCount": "{count} document(s)",
"chunkCount": "{count} chunk(s)"
},
"pluginSetSelector": {
"notSelected": "No plugins enabled",
"allPlugins": "All plugins enabled (*)",
"selectedCount": "{count} plugin(s) selected",
"buttonText": "Select Plugin Set...",
"dialogTitle": "Select Plugin Set",
"loading": "Loading...",
"enableAll": "Enable all plugins",
"enableNone": "Disable all plugins",
"customSelect": "Custom selection",
"noPlugins": "No plugins available",
"confirmSelection": "Confirm Selection",
"cancelSelection": "Cancel",
"noDescription": "No description",
"notActivated": "Not activated",
"note": "*System plugins and disabled plugins are not shown."
},
"providerSelector": {
"notSelected": "Not selected",
"buttonText": "Select Provider...",
"dialogTitle": "Select Provider",
"loading": "Loading...",
"noProviders": "No providers available",
"confirmSelection": "Confirm Selection",
"cancelSelection": "Cancel",
"clearSelection": "None",
"clearSelectionSubtitle": "Clear current selection",
"unknownType": "Unknown type"
}
}

View File

@@ -85,5 +85,9 @@
"reconnected": "Chat connection re-established",
"failed": "Connection failed, please refresh the page"
}
},
"errors": {
"sendMessageFailed": "Failed to send message, please try again",
"createSessionFailed": "Failed to create session, please refresh the page"
}
}

View File

@@ -0,0 +1,476 @@
{
"ai_group": {
"name": "AI",
"agent_runner": {
"description": "Agent Runner",
"hint": "Select the runner for AI conversations. Defaults to AstrBot's built-in Agent runner, which supports knowledge base, persona, and tool calling features. You don't need to modify this section unless you plan to integrate third-party Agent runners like Dify or Coze.",
"provider_settings": {
"enable": {
"description": "Enable",
"hint": "Master switch for AI conversations"
},
"agent_runner_type": {
"description": "Runner",
"labels": ["Built-in Agent", "Dify", "Coze", "Alibaba Cloud Bailian Application"]
},
"coze_agent_runner_provider_id": {
"description": "Coze Agent Runner Provider ID"
},
"dify_agent_runner_provider_id": {
"description": "Dify Agent Runner Provider ID"
},
"dashscope_agent_runner_provider_id": {
"description": "Alibaba Cloud Bailian Application Agent Runner Provider ID"
}
}
},
"ai": {
"description": "Model",
"hint": "When using non-built-in Agent runners, the default chat model and default image caption model may not take effect, but some plugins rely on these settings to invoke AI capabilities.",
"provider_settings": {
"default_provider_id": {
"description": "Default Chat Model",
"hint": "Uses the first model when left empty"
},
"default_image_caption_provider_id": {
"description": "Default Image Caption Model",
"hint": "Leave empty to disable; useful for non-multimodal models"
},
"image_caption_prompt": {
"description": "Image Caption Prompt"
}
},
"provider_stt_settings": {
"enable": {
"description": "Enable Speech-to-Text",
"hint": "Master switch for STT"
},
"provider_id": {
"description": "Default Speech-to-Text Model",
"hint": "Users can also select session-specific STT models using the /provider command."
}
},
"provider_tts_settings": {
"enable": {
"description": "Enable Text-to-Speech",
"hint": "Master switch for TTS"
},
"provider_id": {
"description": "Default Text-to-Speech Model"
}
}
},
"persona": {
"description": "Persona",
"provider_settings": {
"default_personality": {
"description": "Default Persona"
}
}
},
"knowledgebase": {
"description": "Knowledge Base",
"kb_names": {
"description": "Knowledge Base List",
"hint": "Supports multiple selections"
},
"kb_fusion_top_k": {
"description": "Fusion Search Results Count",
"hint": "Number of results returned after fusing search results from multiple knowledge bases"
},
"kb_final_top_k": {
"description": "Final Results Count",
"hint": "Number of results retrieved from the knowledge base. Higher values may provide more relevant information but could also introduce noise. Adjust based on actual needs"
},
"kb_agentic_mode": {
"description": "Agentic Knowledge Base Retrieval",
"hint": "When enabled, knowledge base retrieval becomes an LLM Tool, allowing the model to autonomously decide when to query the knowledge base. Requires the model to support function calling."
}
},
"websearch": {
"description": "Web Search",
"provider_settings": {
"web_search": {
"description": "Enable Web Search"
},
"websearch_provider": {
"description": "Web Search Provider"
},
"websearch_tavily_key": {
"description": "Tavily API Key",
"hint": "Multiple keys can be added for rotation."
},
"websearch_baidu_app_builder_key": {
"description": "Baidu Qianfan Smart Cloud APP Builder API Key",
"hint": "Reference: https://console.bce.baidu.com/iam/#/iam/apikey/list"
},
"web_search_link": {
"description": "Display Source Citations"
}
}
},
"file_extract": {
"description": "File Extract",
"provider_settings": {
"file_extract": {
"enable": {
"description": "Enable File Extract"
},
"provider": {
"description": "File Extract Provider"
},
"moonshotai_api_key": {
"description": "Moonshot AI API Key"
}
}
}
},
"others": {
"description": "Other Settings",
"provider_settings": {
"display_reasoning_text": {
"description": "Display Reasoning Content"
},
"identifier": {
"description": "User Identification",
"hint": "When enabled, user ID information will be included in the prompt."
},
"group_name_display": {
"description": "Display Group Name",
"hint": "When enabled, group name information will be included in the prompt on supported platforms (OneBot v11)."
},
"datetime_system_prompt": {
"description": "Real-world Time Awareness",
"hint": "When enabled, current time information will be appended to the system prompt."
},
"show_tool_use_status": {
"description": "Output Function Call Status"
},
"max_agent_step": {
"description": "Maximum Tool Call Rounds"
},
"tool_call_timeout": {
"description": "Tool Call Timeout (seconds)"
},
"streaming_response": {
"description": "Streaming Output"
},
"unsupported_streaming_strategy": {
"description": "Platforms Without Streaming Support",
"hint": "Select the handling method for platforms that don't support streaming responses. Real-time segmented reply sends content immediately when the system detects segment points like punctuation during streaming reception",
"labels": ["Real-time Segmented Reply", "Disable Streaming Response"]
},
"max_context_length": {
"description": "Maximum Conversation Rounds",
"hint": "Discards the oldest parts when this count is exceeded. One conversation round counts as 1, -1 means unlimited"
},
"dequeue_context_length": {
"description": "Dequeue Conversation Rounds",
"hint": "Number of conversation rounds to discard at once when maximum context length is exceeded"
},
"wake_prefix": {
"description": "Additional LLM Chat Wake Prefix",
"hint": "If the wake prefix is / and the additional chat wake prefix is chat, then /chat is required to trigger LLM requests"
},
"prompt_prefix": {
"description": "User Prompt",
"hint": "You can use {{prompt}} as a placeholder for user input. If no placeholder is provided, it will be added before the user input."
},
"reachability_check": {
"description": "Provider Reachability Check",
"hint": "When running the /provider command, test provider connectivity in parallel. This actively pings models and may consume extra tokens."
}
},
"provider_tts_settings": {
"dual_output": {
"description": "Output Both Voice and Text When TTS is Enabled"
}
}
}
},
"platform_group": {
"name": "Platform",
"general": {
"description": "General",
"admins_id": {
"description": "Administrator IDs"
},
"platform_settings": {
"unique_session": {
"description": "Isolate Sessions",
"hint": "When enabled, group members have independent contexts."
},
"friend_message_needs_wake_prefix": {
"description": "Private Messages Require Wake Word"
},
"reply_prefix": {
"description": "Reply Text Prefix"
},
"reply_with_mention": {
"description": "Mention Sender in Reply"
},
"reply_with_quote": {
"description": "Quote Sender's Message in Reply"
},
"forward_threshold": {
"description": "Forward Message Word Count Threshold"
},
"empty_mention_waiting": {
"description": "Trigger Waiting on Mention-only Messages"
}
},
"wake_prefix": {
"description": "Wake Word"
}
},
"whitelist": {
"description": "Whitelist",
"platform_settings": {
"enable_id_white_list": {
"description": "Enable Whitelist",
"hint": "When enabled, only sessions in the whitelist will be responded to."
},
"id_whitelist": {
"description": "Whitelist ID List",
"hint": "Use /sid to get IDs."
},
"id_whitelist_log": {
"description": "Output Logs",
"hint": "When enabled, INFO level logs will be output when a message doesn't pass the whitelist."
},
"wl_ignore_admin_on_group": {
"description": "Administrator Group Messages Bypass ID Whitelist"
},
"wl_ignore_admin_on_friend": {
"description": "Administrator Private Messages Bypass ID Whitelist"
}
}
},
"rate_limit": {
"description": "Rate Limiting",
"platform_settings": {
"rate_limit": {
"time": {
"description": "Message Rate Limit Time (seconds)"
},
"count": {
"description": "Message Rate Limit Count"
},
"strategy": {
"description": "Rate Limit Strategy"
}
}
}
},
"content_safety": {
"description": "Content Safety",
"content_safety": {
"also_use_in_response": {
"description": "Also Check Model Response Content"
},
"baidu_aip": {
"enable": {
"description": "Use Baidu Content Safety Moderation",
"hint": "You need to manually install the baidu-aip library."
},
"app_id": {
"description": "App ID"
},
"api_key": {
"description": "API Key"
},
"secret_key": {
"description": "Secret Key"
}
},
"internal_keywords": {
"enable": {
"description": "Keyword Check"
},
"extra_keywords": {
"description": "Additional Keywords",
"hint": "Additional keyword blocklist, supports regular expressions."
}
}
}
},
"t2i": {
"description": "Text-to-Image",
"t2i": {
"description": "Text-to-Image Output"
},
"t2i_word_threshold": {
"description": "Text-to-Image Word Count Threshold"
}
},
"others": {
"description": "Other Settings",
"platform_settings": {
"ignore_bot_self_message": {
"description": "Ignore Bot's Own Messages"
},
"ignore_at_all": {
"description": "Ignore @All Events"
},
"no_permission_reply": {
"description": "Reply When User Has Insufficient Permissions"
}
},
"platform_specific": {
"lark": {
"pre_ack_emoji": {
"enable": {
"description": "[Lark] Enable Pre-acknowledgment Emoji"
},
"emojis": {
"description": "Emoji List (Lark Emoji Enum Names)",
"hint": "Emoji enum names reference: https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce"
}
}
},
"telegram": {
"pre_ack_emoji": {
"enable": {
"description": "[Telegram] Enable Pre-acknowledgment Emoji"
},
"emojis": {
"description": "Emoji List (Unicode)",
"hint": "Telegram only supports a fixed reaction set, reference: https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9"
}
}
}
}
}
},
"plugin_group": {
"name": "Plugin",
"plugin": {
"description": "Plugins",
"plugin_set": {
"description": "Available Plugins",
"hint": "All non-disabled plugins are enabled by default. If a plugin is disabled on the plugins page, selections here will not take effect."
}
}
},
"ext_group": {
"name": "Ext.",
"segmented_reply": {
"description": "Segmented Reply",
"platform_settings": {
"segmented_reply": {
"enable": {
"description": "Enable Segmented Reply"
},
"only_llm_result": {
"description": "Segment Only LLM Results"
},
"interval_method": {
"description": "Interval Method"
},
"interval": {
"description": "Random Interval Time",
"hint": "Format: minimum,maximum (e.g., 1.5,3.5)"
},
"log_base": {
"description": "Logarithm Base",
"hint": "Base for logarithmic intervals, defaults to 2.0. Value range: 1.0-10.0."
},
"words_count_threshold": {
"description": "Segmented Reply Word Count Threshold"
},
"regex": {
"description": "Segmentation Regular Expression"
},
"content_cleanup_rule": {
"description": "Content Filtering Regular Expression",
"hint": "Remove specified content from segmented content. For example, `[。?!]` will remove all periods, question marks, and exclamation marks."
}
}
}
},
"ltm": {
"description": "Group Chat Context Awareness (formerly Chat Memory Enhancement)",
"provider_ltm_settings": {
"group_icl_enable": {
"description": "Enable Group Chat Context Awareness"
},
"group_message_max_cnt": {
"description": "Maximum Message Count"
},
"image_caption": {
"description": "Auto-understand Images",
"hint": "Requires setting a group chat image caption model."
},
"image_caption_provider_id": {
"description": "Group Chat Image Caption Model",
"hint": "Used for image understanding in group chat context awareness, configured separately from the default image caption model."
},
"active_reply": {
"enable": {
"description": "Active Reply"
},
"method": {
"description": "Active Reply Method"
},
"possibility_reply": {
"description": "Reply Probability",
"hint": "Value between 0.0-1.0"
},
"whitelist": {
"description": "Active Reply Whitelist",
"hint": "Whitelist filtering is disabled when empty. Use /sid to get IDs."
}
}
}
}
},
"system_group": {
"name": "System",
"system": {
"description": "System Settings",
"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."
},
"t2i_endpoint": {
"description": "Text-to-Image Service API Endpoint",
"hint": "Uses AstrBot API service when empty"
},
"t2i_template": {
"description": "Text-to-Image Custom Template",
"hint": "When enabled, you can customize HTML templates for text-to-image rendering."
},
"t2i_active_template": {
"description": "Currently Active Text-to-Image Rendering Template",
"hint": "This value is maintained by the text-to-image template management page."
},
"log_level": {
"description": "Console Log Level",
"hint": "Log level for console output."
},
"pip_install_arg": {
"description": "Additional pip Installation Arguments",
"hint": "When installing plugin dependencies, Python's pip tool will be used. Additional arguments can be provided here, such as `--break-system-package`."
},
"pypi_index_url": {
"description": "PyPI Repository URL",
"hint": "PyPI repository URL for installing Python dependencies. Defaults to https://mirrors.aliyun.com/pypi/simple/"
},
"callback_api_base": {
"description": "Externally Accessible Callback API Address",
"hint": "External services may access AstrBot's backend through callback links generated by AstrBot (such as file download links). Since AstrBot cannot automatically determine the externally accessible host address in the deployment environment, this configuration item is needed to explicitly specify how external services should access AstrBot's address. Examples: http://localhost:6185, https://example.com, etc."
},
"timezone": {
"description": "Timezone",
"hint": "Timezone setting. Please enter an IANA timezone name, such as Asia/Shanghai. Uses system default timezone when empty. For all timezones, see: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab"
},
"http_proxy": {
"description": "HTTP Proxy",
"hint": "When enabled, proxy will be set by adding environment variables. Format: `http://ip:port`"
},
"no_proxy": {
"description": "Direct Connection Address List"
}
}
}
}

View File

@@ -62,5 +62,33 @@
"allowedHosts": "Allowed Hosts",
"rateLimit": "Rate Limit",
"encryption": "Encryption Settings"
},
"configSelection": {
"selectConfig": "Select Configuration",
"normalConfig": "Basic",
"systemConfig": "System"
},
"configManagement": {
"title": "Configuration Management",
"description": "AstrBot supports separate configuration files for different bots. The `default` configuration is used by default.",
"newConfig": "New Configuration",
"editConfig": "Edit Configuration",
"manageConfigs": "Manage Configurations...",
"configName": "Name",
"fillConfigName": "Enter configuration name",
"confirmDelete": "Are you sure you want to delete the configuration \"{name}\"? This action cannot be undone.",
"pleaseEnterName": "Please enter a configuration name",
"createFailed": "Failed to create new configuration",
"deleteFailed": "Failed to delete configuration",
"updateFailed": "Failed to update configuration"
},
"buttons": {
"cancel": "Cancel",
"create": "Create",
"update": "Update"
},
"codeEditor": {
"title": "Edit Configuration File"
}
}

View File

@@ -32,7 +32,8 @@
"actions": "Actions",
"back": "Back",
"selectFile": "Select File",
"refresh": "Refresh"
"refresh": "Refresh",
"updateAll": "Update All"
},
"status": {
"enabled": "Enabled",
@@ -141,7 +142,9 @@
"confirmDelete": "Are you sure you want to delete this extension?",
"fillUrlOrFile": "Please fill in extension URL or upload extension file",
"dontFillBoth": "Please don't fill in both extension URL and upload file",
"supportedFormats": "Supports .zip extension files"
"supportedFormats": "Supports .zip extension files",
"updateAllSuccess": "All upgradable extensions have been updated!",
"updateAllFailed": "{failed} of {total} extensions failed to update:"
},
"upload": {
"fromFile": "Install from File",

View File

@@ -1,124 +1,110 @@
{
"title": "Session Management",
"subtitle": "Manage active sessions and configurations",
"title": "Custom Rules",
"subtitle": "Set custom rules for specific sessions, which take priority over global settings",
"buttons": {
"refresh": "Refresh",
"edit": "Edit",
"apply": "Apply Batch Settings",
"editName": "Edit Session Name",
"editRule": "Edit Rules",
"deleteAllRules": "Delete All Rules",
"addRule": "Add Rule",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
"delete": "Delete",
"clear": "Clear",
"next": "Next",
"editCustomName": "Edit Note",
"batchDelete": "Batch Delete"
},
"sessions": {
"activeSessions": "Active Sessions",
"sessionCount": "sessions",
"noActiveSessions": "No active sessions",
"noActiveSessionsDesc": "Sessions will appear here when users interact with the bot"
"customRules": {
"title": "Custom Rules",
"rulesCount": "rules",
"hasRules": "Configured",
"noRules": "No Custom Rules",
"noRulesDesc": "Click 'Add Rule' to configure custom rules for specific sessions",
"serviceConfig": "Service Config",
"pluginConfig": "Plugin Config",
"kbConfig": "Knowledge Base",
"providerConfig": "Provider Config",
"configured": "Configured",
"noCustomName": "No note set"
},
"quickEditName": {
"title": "Edit Note"
},
"search": {
"placeholder": "Search sessions...",
"platformFilter": "Platform Filter"
"placeholder": "Search sessions..."
},
"table": {
"headers": {
"sessionStatus": "Session Status",
"sessionInfo": "Session Info",
"persona": "Persona",
"chatProvider": "Chat Provider",
"sttProvider": "STT Provider",
"ttsProvider": "TTS Provider",
"llmStatus": "LLM Status",
"ttsStatus": "TTS Status",
"knowledgeBase": "Knowledge Base",
"pluginManagement": "Plugin Management",
"umoInfo": "Unified Message Origin",
"rulesOverview": "Rules Overview",
"actions": "Actions"
}
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled"
},
"persona": {
"none": "No Persona"
"none": "Follow Config"
},
"batchOperations": {
"title": "Batch Operations",
"setPersona": "Batch Set Persona",
"setChatProvider": "Batch Set Chat Provider",
"setSttProvider": "Batch Set STT Provider",
"setTtsProvider": "Batch Set TTS Provider",
"setLlmStatus": "Batch Set LLM Status",
"setTtsStatus": "Batch Set TTS Status",
"noSttProvider": "No STT Provider Available",
"noTtsProvider": "No TTS Provider Available"
"provider": {
"followConfig": "Follow Config"
},
"pluginManagement": {
"title": "Plugin Management",
"noPlugins": "No available plugins",
"noPluginsDesc": "Currently no active plugins",
"loading": "Loading plugin list...",
"author": "Author"
"addRule": {
"title": "Add Custom Rule",
"description": "Select a session (UMO) to configure custom rules. Custom rules take priority over global settings.",
"selectUmo": "Select Session",
"noUmos": "No sessions available"
},
"nameEditor": {
"title": "Edit Session Name",
"customName": "Custom Name",
"placeholder": "Enter custom session name (leave empty to use original name)",
"originalName": "Original Name",
"fullSessionId": "Full Session ID",
"hint": "Custom names help you easily identify sessions. The small information icon (!) will show the actual UMO when hovering."
},
"knowledgeBase": {
"title": "Knowledge Base Configuration",
"configure": "Configure",
"selectKB": "Select Knowledge Bases",
"selectMultiple": "You can select multiple knowledge bases",
"noKBAvailable": "No knowledge bases available",
"noKBDesc": "No knowledge bases have been created yet",
"createKB": "Create Knowledge Base",
"advancedSettings": "Advanced Settings",
"topK": "Result Count",
"topKHint": "Number of results to retrieve from knowledge base",
"enableRerank": "Enable Reranking",
"enableRerankHint": "Use reranking model to improve retrieval quality",
"clearConfig": "Clear Configuration",
"save": "Save",
"cancel": "Cancel",
"loading": "Loading knowledge base configuration...",
"description": "Configure knowledge bases for this session. The session will use configured knowledge bases to enhance conversation context.",
"saveSuccess": "Knowledge base configuration saved successfully",
"saveFailed": "Failed to save knowledge base configuration",
"loadFailed": "Failed to load knowledge base configuration",
"clearSuccess": "Knowledge base configuration cleared",
"clearFailed": "Failed to clear knowledge base configuration",
"clearConfirm": "Are you sure you want to clear the knowledge base configuration for this session?"
},
"list": {
"documents": "documents"
"ruleEditor": {
"title": "Edit Custom Rules",
"description": "Configure custom rules for this session. These rules take priority over global settings.",
"serviceConfig": {
"title": "Service Configuration",
"sessionEnabled": "Enable Session",
"llmEnabled": "Enable LLM",
"ttsEnabled": "Enable TTS",
"customName": "Custom Name"
},
"providerConfig": {
"title": "Provider Configuration",
"chatProvider": "Chat Provider",
"sttProvider": "STT Provider",
"ttsProvider": "TTS Provider"
},
"personaConfig": {
"title": "Persona Configuration",
"selectPersona": "Select Persona",
"hint": "Persona settings affect the conversation style and behavior of the LLM"
},
"pluginConfig": {
"title": "Plugin Configuration",
"disabledPlugins": "Disabled Plugins",
"hint": "Select plugins to disable for this session. Unselected plugins will remain enabled."
},
"kbConfig": {
"title": "Knowledge Base Configuration",
"selectKbs": "Select Knowledge Bases",
"topK": "Top K Results",
"enableRerank": "Enable Reranking"
}
},
"deleteConfirm": {
"message": "Are you sure you want to delete session {sessionName}?",
"warning": "This action will permanently delete all chat history and preference settings for this session (except for data linked via plugins), and this cannot be undone. Continue?"
"title": "Confirm Delete",
"message": "Are you sure you want to delete all custom rules for this session? Global settings will be used after deletion."
},
"batchDeleteConfirm": {
"title": "Confirm Batch Delete",
"message": "Are you sure you want to delete {count} selected rules? Global settings will be used after deletion."
},
"messages": {
"refreshSuccess": "Session list refreshed",
"personaUpdateSuccess": "Persona updated successfully",
"personaUpdateError": "Failed to update persona",
"providerUpdateSuccess": "Provider updated successfully",
"providerUpdateError": "Failed to update provider",
"sessionStatusSuccess": "Session {status}",
"llmStatusSuccess": "LLM {status}",
"ttsStatusSuccess": "TTS {status}",
"statusUpdateError": "Failed to update status",
"loadSessionsError": "Failed to load session list",
"batchUpdateSuccess": "Successfully batch updated {count} settings",
"batchUpdatePartial": "Batch update completed, {success} successful, {error} failed",
"loadPluginsError": "Failed to load plugin list",
"pluginStatusSuccess": "Plugin {name} {status}",
"pluginStatusError": "Failed to update plugin status",
"nameUpdateSuccess": "Session name updated successfully",
"nameUpdateError": "Failed to update session name",
"deleteSuccess": "Session deleted successfully",
"deleteError": "Failed to delete session"
"refreshSuccess": "Data refreshed",
"loadError": "Failed to load data",
"saveSuccess": "Saved successfully",
"saveError": "Failed to save",
"clearSuccess": "Cleared successfully",
"clearError": "Failed to clear",
"deleteSuccess": "Deleted successfully",
"deleteError": "Failed to delete",
"noChanges": "No changes to save",
"batchDeleteSuccess": "Batch delete successful",
"batchDeleteError": "Batch delete failed"
}
}

View File

@@ -8,7 +8,7 @@
"config": "配置文件",
"chat": "聊天",
"conversation": "对话数据",
"sessionManagement": "会话管理",
"sessionManagement": "自定义规则",
"console": "控制台",
"alkaid": "Alkaid",
"knowledgeBase": "知识库",

View File

@@ -0,0 +1,45 @@
{
"knowledgeBaseSelector": {
"notSelected": "未选择",
"buttonText": "选择知识库...",
"dialogTitle": "选择知识库",
"loading": "加载中...",
"noKnowledgeBases": "暂无知识库",
"createKnowledgeBase": "创建知识库",
"selectedCount": "已选择 {count} 个知识库",
"confirmSelection": "确认选择",
"cancelSelection": "取消",
"noDescription": "无描述",
"documentCount": "{count} 个文档",
"chunkCount": "{count} 个块"
},
"pluginSetSelector": {
"notSelected": "未启用任何插件",
"allPlugins": "启用所有插件 (*)",
"selectedCount": "已选择 {count} 个插件",
"buttonText": "选择插件集合...",
"dialogTitle": "选择插件集合",
"loading": "加载中...",
"enableAll": "启用所有插件",
"enableNone": "不启用任何插件",
"customSelect": "自定义选择",
"noPlugins": "暂无可用的插件",
"confirmSelection": "确认选择",
"cancelSelection": "取消",
"noDescription": "无描述",
"notActivated": "未激活",
"note": "*不显示系统插件和已经在插件页禁用的插件。"
},
"providerSelector": {
"notSelected": "未选择",
"buttonText": "选择提供商...",
"dialogTitle": "选择提供商",
"loading": "加载中...",
"noProviders": "暂无可用的提供商",
"confirmSelection": "确认选择",
"cancelSelection": "取消",
"clearSelection": "不选择",
"clearSelectionSubtitle": "清除当前选择",
"unknownType": "未知类型"
}
}

View File

@@ -85,5 +85,9 @@
"reconnected": "聊天连接已重新建立",
"failed": "连接失败,请刷新页面重试"
}
},
"errors": {
"sendMessageFailed": "发送消息失败,请重试",
"createSessionFailed": "创建会话失败,请刷新页面重试"
}
}

View File

@@ -0,0 +1,484 @@
{
"ai_group": {
"name": "AI 配置",
"agent_runner": {
"description": "Agent 执行方式",
"hint": "选择 AI 对话的执行器,默认为 AstrBot 内置 Agent 执行器,可使用 AstrBot 内的知识库、人格、工具调用功能。如果不打算接入 Dify 或 Coze 等第三方 Agent 执行器,不需要修改此节。",
"provider_settings": {
"enable": {
"description": "启用",
"hint": "AI 对话总开关"
},
"agent_runner_type": {
"description": "执行器",
"labels": [
"内置 Agent",
"Dify",
"Coze",
"阿里云百炼应用"
]
},
"coze_agent_runner_provider_id": {
"description": "Coze Agent 执行器提供商 ID"
},
"dify_agent_runner_provider_id": {
"description": "Dify Agent 执行器提供商 ID"
},
"dashscope_agent_runner_provider_id": {
"description": "阿里云百炼应用 Agent 执行器提供商 ID"
}
}
},
"ai": {
"description": "模型",
"hint": "当使用非内置 Agent 执行器时,默认聊天模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
"provider_settings": {
"default_provider_id": {
"description": "默认聊天模型",
"hint": "留空时使用第一个模型"
},
"default_image_caption_provider_id": {
"description": "默认图片转述模型",
"hint": "留空代表不使用,可用于非多模态模型"
},
"image_caption_prompt": {
"description": "图片转述提示词"
}
},
"provider_stt_settings": {
"enable": {
"description": "启用语音转文本",
"hint": "STT 总开关"
},
"provider_id": {
"description": "默认语音转文本模型",
"hint": "用户也可使用 /provider 指令单独选择会话的 STT 模型。"
}
},
"provider_tts_settings": {
"enable": {
"description": "启用文本转语音",
"hint": "TTS 总开关"
},
"provider_id": {
"description": "默认文本转语音模型"
}
}
},
"persona": {
"description": "人格",
"provider_settings": {
"default_personality": {
"description": "默认采用的人格"
}
}
},
"knowledgebase": {
"description": "知识库",
"kb_names": {
"description": "知识库列表",
"hint": "支持多选"
},
"kb_fusion_top_k": {
"description": "融合检索结果数",
"hint": "多个知识库检索结果融合后的返回结果数量"
},
"kb_final_top_k": {
"description": "最终返回结果数",
"hint": "从知识库中检索到的结果数量,越大可能获得越多相关信息,但也可能引入噪音。建议根据实际需求调整"
},
"kb_agentic_mode": {
"description": "Agentic 知识库检索",
"hint": "启用后,知识库检索将作为 LLM Tool,由模型自主决定何时调用知识库进行查询。需要模型支持函数调用能力。"
}
},
"websearch": {
"description": "网页搜索",
"provider_settings": {
"web_search": {
"description": "启用网页搜索"
},
"websearch_provider": {
"description": "网页搜索提供商"
},
"websearch_tavily_key": {
"description": "Tavily API Key",
"hint": "可添加多个 Key 进行轮询。"
},
"websearch_baidu_app_builder_key": {
"description": "百度千帆智能云 APP Builder API Key",
"hint": "参考:https://console.bce.baidu.com/iam/#/iam/apikey/list"
},
"web_search_link": {
"description": "显示来源引用"
}
}
},
"file_extract": {
"description": "文档解析能力",
"provider_settings": {
"file_extract": {
"enable": {
"description": "启用文档解析能力"
},
"provider": {
"description": "文档解析提供商"
},
"moonshotai_api_key": {
"description": "Moonshot AI API Key"
}
}
}
},
"others": {
"description": "其他配置",
"provider_settings": {
"display_reasoning_text": {
"description": "显示思考内容"
},
"identifier": {
"description": "用户识别",
"hint": "启用后,会在提示词前包含用户 ID 信息。"
},
"group_name_display": {
"description": "显示群名称",
"hint": "启用后,在支持的平台(OneBot v11)上会在提示词前包含群名称信息。"
},
"datetime_system_prompt": {
"description": "现实世界时间感知",
"hint": "启用后,会在系统提示词中附带当前时间信息。"
},
"show_tool_use_status": {
"description": "输出函数调用状态"
},
"max_agent_step": {
"description": "工具调用轮数上限"
},
"tool_call_timeout": {
"description": "工具调用超时时间(秒)"
},
"streaming_response": {
"description": "流式输出"
},
"unsupported_streaming_strategy": {
"description": "不支持流式回复的平台",
"hint": "选择在不支持流式回复的平台上的处理方式。实时分段回复会在系统接收流式响应检测到诸如标点符号等分段点时,立即发送当前已接收的内容",
"labels": [
"实时分段回复",
"关闭流式回复"
]
},
"max_context_length": {
"description": "最多携带对话轮数",
"hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条,-1 为不限制"
},
"dequeue_context_length": {
"description": "丢弃对话轮数",
"hint": "超出最多携带对话轮数时, 一次丢弃的聊天轮数"
},
"wake_prefix": {
"description": "LLM 聊天额外唤醒前缀",
"hint": "如果唤醒前缀为 /, 额外聊天唤醒前缀为 chat,则需要 /chat 才会触发 LLM 请求"
},
"prompt_prefix": {
"description": "用户提示词",
"hint": "可使用 {{prompt}} 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。"
},
"reachability_check": {
"description": "提供商可达性检测",
"hint": "/provider 命令列出模型时并发检测连通性。开启后会主动调用模型测试连通性,可能产生额外 token 消耗。"
}
},
"provider_tts_settings": {
"dual_output": {
"description": "开启 TTS 时同时输出语音和文字内容"
}
}
}
},
"platform_group": {
"name": "平台配置",
"general": {
"description": "基本",
"admins_id": {
"description": "管理员 ID"
},
"platform_settings": {
"unique_session": {
"description": "隔离会话",
"hint": "启用后,群成员的上下文独立。"
},
"friend_message_needs_wake_prefix": {
"description": "私聊消息需要唤醒词"
},
"reply_prefix": {
"description": "回复时的文本前缀"
},
"reply_with_mention": {
"description": "回复时 @ 发送人"
},
"reply_with_quote": {
"description": "回复时引用发送人消息"
},
"forward_threshold": {
"description": "转发消息的字数阈值"
},
"empty_mention_waiting": {
"description": "只 @ 机器人是否触发等待"
}
},
"wake_prefix": {
"description": "唤醒词"
}
},
"whitelist": {
"description": "白名单",
"platform_settings": {
"enable_id_white_list": {
"description": "启用白名单",
"hint": "启用后,只有在白名单内的会话会被响应。"
},
"id_whitelist": {
"description": "白名单 ID 列表",
"hint": "使用 /sid 获取 ID。"
},
"id_whitelist_log": {
"description": "输出日志",
"hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。"
},
"wl_ignore_admin_on_group": {
"description": "管理员群组消息无视 ID 白名单"
},
"wl_ignore_admin_on_friend": {
"description": "管理员私聊消息无视 ID 白名单"
}
}
},
"rate_limit": {
"description": "速率限制",
"platform_settings": {
"rate_limit": {
"time": {
"description": "消息速率限制时间(秒)"
},
"count": {
"description": "消息速率限制计数"
},
"strategy": {
"description": "速率限制策略"
}
}
}
},
"content_safety": {
"description": "内容安全",
"content_safety": {
"also_use_in_response": {
"description": "同时检查模型的响应内容"
},
"baidu_aip": {
"enable": {
"description": "使用百度内容安全审核",
"hint": "您需要手动安装 baidu-aip 库。"
},
"app_id": {
"description": "App ID"
},
"api_key": {
"description": "API Key"
},
"secret_key": {
"description": "Secret Key"
}
},
"internal_keywords": {
"enable": {
"description": "关键词检查"
},
"extra_keywords": {
"description": "额外关键词",
"hint": "额外的屏蔽关键词列表,支持正则表达式。"
}
}
}
},
"t2i": {
"description": "文本转图像",
"t2i": {
"description": "文本转图像输出"
},
"t2i_word_threshold": {
"description": "文本转图像字数阈值"
}
},
"others": {
"description": "其他配置",
"platform_settings": {
"ignore_bot_self_message": {
"description": "是否忽略机器人自身的消息"
},
"ignore_at_all": {
"description": "是否忽略 @ 全体成员事件"
},
"no_permission_reply": {
"description": "用户权限不足时是否回复"
}
},
"platform_specific": {
"lark": {
"pre_ack_emoji": {
"enable": {
"description": "[飞书] 启用预回应表情"
},
"emojis": {
"description": "表情列表(飞书表情枚举名)",
"hint": "表情枚举名参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce"
}
}
},
"telegram": {
"pre_ack_emoji": {
"enable": {
"description": "[Telegram] 启用预回应表情"
},
"emojis": {
"description": "表情列表(Unicode)",
"hint": "Telegram 仅支持固定反应集合,参考:https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9"
}
}
}
}
}
},
"plugin_group": {
"name": "插件配置",
"plugin": {
"description": "插件",
"plugin_set": {
"description": "可用插件",
"hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。"
}
}
},
"ext_group": {
"name": "扩展功能",
"segmented_reply": {
"description": "分段回复",
"platform_settings": {
"segmented_reply": {
"enable": {
"description": "启用分段回复"
},
"only_llm_result": {
"description": "仅对 LLM 结果分段"
},
"interval_method": {
"description": "间隔方法"
},
"interval": {
"description": "随机间隔时间",
"hint": "格式:最小值,最大值(如:1.5,3.5)"
},
"log_base": {
"description": "对数底数",
"hint": "对数间隔的底数,默认为 2.0。取值范围为 1.0-10.0。"
},
"words_count_threshold": {
"description": "分段回复字数阈值"
},
"regex": {
"description": "分段正则表达式"
},
"content_cleanup_rule": {
"description": "内容过滤正则表达式",
"hint": "移除分段后内容中的指定内容。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。"
}
}
}
},
"ltm": {
"description": "群聊上下文感知(原聊天记忆增强)",
"provider_ltm_settings": {
"group_icl_enable": {
"description": "启用群聊上下文感知"
},
"group_message_max_cnt": {
"description": "最大消息数量"
},
"image_caption": {
"description": "自动理解图片",
"hint": "需要设置群聊图片转述模型。"
},
"image_caption_provider_id": {
"description": "群聊图片转述模型",
"hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。"
},
"active_reply": {
"enable": {
"description": "主动回复"
},
"method": {
"description": "主动回复方法"
},
"possibility_reply": {
"description": "回复概率",
"hint": "0.0-1.0 之间的数值"
},
"whitelist": {
"description": "主动回复白名单",
"hint": "为空时不启用白名单过滤。使用 /sid 获取 ID。"
}
}
}
}
},
"system_group": {
"name": "系统配置",
"system": {
"description": "系统配置",
"t2i_strategy": {
"description": "文本转图像策略",
"hint": "文本转图像策略。`remote` 为使用远程基于 HTML 的渲染服务,`local` 为使用 PIL 本地渲染。当使用 local 时,将 ttf 字体命名为 'font.ttf' 放在 data/ 目录下可自定义字体。"
},
"t2i_endpoint": {
"description": "文本转图像服务 API 地址",
"hint": "为空时使用 AstrBot API 服务"
},
"t2i_template": {
"description": "文本转图像自定义模版",
"hint": "启用后可自定义 HTML 模板用于文转图渲染。"
},
"t2i_active_template": {
"description": "当前应用的文转图渲染模板",
"hint": "此处的值由文转图模板管理页面进行维护。"
},
"log_level": {
"description": "控制台日志级别",
"hint": "控制台输出日志的级别。"
},
"pip_install_arg": {
"description": "pip 安装额外参数",
"hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。"
},
"pypi_index_url": {
"description": "PyPI 软件仓库地址",
"hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 https://mirrors.aliyun.com/pypi/simple/"
},
"callback_api_base": {
"description": "对外可达的回调接口地址",
"hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定外部服务如何访问 AstrBot 的地址。如 http://localhost:6185,https://example.com 等。"
},
"timezone": {
"description": "时区",
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab"
},
"http_proxy": {
"description": "HTTP 代理",
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`"
},
"no_proxy": {
"description": "直连地址列表"
}
}
}
}

View File

@@ -62,5 +62,32 @@
"allowedHosts": "允许的主机",
"rateLimit": "频率限制",
"encryption": "加密设置"
},
"configSelection": {
"selectConfig": "选择配置文件",
"normalConfig": "普通",
"systemConfig": "系统"
},
"configManagement": {
"title": "配置文件管理",
"description": "AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。",
"newConfig": "新建配置文件",
"editConfig": "编辑配置文件",
"manageConfigs": "管理配置文件...",
"configName": "名称",
"fillConfigName": "填写配置文件名称",
"confirmDelete": "确定要删除配置文件 \"{name}\" 吗?此操作不可恢复。",
"pleaseEnterName": "请填写配置名称",
"createFailed": "新配置文件创建失败",
"deleteFailed": "删除配置文件失败",
"updateFailed": "更新配置文件失败"
},
"buttons": {
"cancel": "取消",
"create": "创建",
"update": "更新"
},
"codeEditor": {
"title": "编辑配置文件"
}
}

View File

@@ -32,7 +32,8 @@
"actions": "操作",
"back": "返回",
"selectFile": "选择文件",
"refresh": "刷新"
"refresh": "刷新",
"updateAll": "更新全部插件"
},
"status": {
"enabled": "启用",
@@ -141,7 +142,9 @@
"confirmDelete": "确定要删除插件吗?",
"fillUrlOrFile": "请填写插件链接或上传插件文件",
"dontFillBoth": "请不要同时填写插件链接和上传文件",
"supportedFormats": "支持 .zip 格式的插件文件"
"supportedFormats": "支持 .zip 格式的插件文件",
"updateAllSuccess": "所有可更新的插件都已更新!",
"updateAllFailed": "有 {failed}/{total} 个插件更新失败:"
},
"upload": {
"fromFile": "从文件安装",

View File

@@ -1,124 +1,110 @@
{
"title": "会话管理",
"subtitle": "管理活跃会话和配置",
"title": "自定义规则",
"subtitle": "为特定会话设置自定义规则,优先级高于全局配置",
"buttons": {
"refresh": "刷新",
"edit": "编辑",
"apply": "应用批量设置",
"editName": "备注",
"editRule": "编辑规则",
"deleteAllRules": "删除所有规则",
"addRule": "添加规则",
"save": "保存",
"cancel": "取消",
"delete": "删除"
"delete": "删除",
"clear": "清除",
"next": "下一步",
"editCustomName": "编辑备注",
"batchDelete": "批量删除"
},
"sessions": {
"activeSessions": "活跃会话",
"sessionCount": "个会话",
"noActiveSessions": "暂无活跃会话",
"noActiveSessionsDesc": "当有用户与机器人交互时,会话将会显示在这里"
"customRules": {
"title": "自定义规则",
"rulesCount": "条规则",
"hasRules": "已配置",
"noRules": "暂无自定义规则",
"noRulesDesc": "点击「添加规则」为特定会话配置自定义规则",
"serviceConfig": "服务配置",
"pluginConfig": "插件配置",
"kbConfig": "知识库配置",
"providerConfig": "模型配置",
"configured": "已配置",
"noCustomName": "未设置备注"
},
"quickEditName": {
"title": "编辑备注名"
},
"search": {
"placeholder": "搜索会话...",
"platformFilter": "平台筛选"
"placeholder": "搜索会话..."
},
"table": {
"headers": {
"sessionStatus": "会话状态",
"sessionInfo": "消息会话来源",
"persona": "人格",
"chatProvider": "聊天模型",
"sttProvider": "语音识别模型",
"ttsProvider": "语音合成模型",
"llmStatus": "启用 LLM",
"ttsStatus": "启用 TTS",
"knowledgeBase": "知识库配置",
"pluginManagement": "插件管理",
"umoInfo": "消息会话来源",
"rulesOverview": "规则概览",
"actions": "操作"
}
},
"status": {
"enabled": "已启用",
"disabled": "已禁用"
},
"persona": {
"none": "无人格"
"none": "跟随配置文件"
},
"batchOperations": {
"title": "批量操作",
"setPersona": "批量设置人格",
"setChatProvider": "批量设置 Chat Provider",
"setSttProvider": "批量设置 STT Provider",
"setTtsProvider": "批量设置 TTS Provider",
"setLlmStatus": "批量设置 LLM 状态",
"setTtsStatus": "批量设置 TTS 状态",
"noSttProvider": "暂无可用 STT Provider",
"noTtsProvider": "暂无可用 TTS Provider"
"provider": {
"followConfig": "跟随配置文件"
},
"pluginManagement": {
"title": "插件管理",
"noPlugins": "暂无可用插件",
"noPluginsDesc": "目前没有激活的插件",
"loading": "加载插件列表中...",
"author": "作者"
"addRule": {
"title": "添加自定义规则",
"description": "选择一个消息会话来源 (UMO) 来配置自定义规则。自定义规则的优先级高于该来源所属的配置文件中的全局规则。可以使用 /sid 指令获取该来源的 UMO 信息。",
"selectUmo": "选择会话",
"noUmos": "暂无可用会话"
},
"nameEditor": {
"title": "编辑会话名称",
"customName": "自定义名称",
"placeholder": "输入自定义会话名称(留空则使用原始名称)",
"originalName": "原始名称",
"fullSessionId": "完整会话ID",
"hint": "自定义名称帮助您轻松识别会话。当设置了自定义名称时会显示一个小感叹号标识鼠标悬停时会显示实际的UMO。"
},
"knowledgeBase": {
"title": "知识库配置",
"configure": "配置",
"selectKB": "选择知识库",
"selectMultiple": "可以选择多个知识库",
"noKBAvailable": "暂无可用的知识库",
"noKBDesc": "目前没有创建任何知识库",
"createKB": "创建知识库",
"advancedSettings": "高级配置",
"topK": "返回结果数量",
"topKHint": "从知识库检索的结果数量",
"enableRerank": "启用重排序",
"enableRerankHint": "使用重排序模型提高检索质量",
"clearConfig": "清除配置",
"save": "保存",
"cancel": "取消",
"loading": "加载知识库配置中...",
"description": "为此会话配置使用的知识库。会话将使用配置的知识库来增强对话上下文。",
"saveSuccess": "知识库配置保存成功",
"saveFailed": "保存知识库配置失败",
"loadFailed": "加载知识库配置失败",
"clearSuccess": "知识库配置已清除",
"clearFailed": "清除知识库配置失败",
"clearConfirm": "确定要清除此会话的知识库配置吗?"
},
"list": {
"documents": "篇文档"
"ruleEditor": {
"title": "编辑自定义规则",
"description": "为此会话配置自定义规则,这些规则将优先于全局配置生效。",
"serviceConfig": {
"title": "服务配置",
"sessionEnabled": "启用该消息会话来源的消息处理",
"llmEnabled": "启用 LLM",
"ttsEnabled": "启用 TTS",
"customName": "消息会话来源备注名称"
},
"providerConfig": {
"title": "模型配置",
"chatProvider": "聊天模型",
"sttProvider": "语音识别模型",
"ttsProvider": "语音合成模型"
},
"personaConfig": {
"title": "人格配置",
"selectPersona": "选择人格",
"hint": "应用人格配置后,将会强制该来源的所有对话使用该人格。"
},
"pluginConfig": {
"title": "插件配置",
"disabledPlugins": "禁用的插件",
"hint": "选择要在此会话中禁用的插件。未选择的插件将保持启用状态。"
},
"kbConfig": {
"title": "知识库配置",
"selectKbs": "选择知识库",
"topK": "返回结果数量 (Top K)",
"enableRerank": "启用重排序"
}
},
"deleteConfirm": {
"message": "确定要删除会话 {sessionName} 吗?",
"warning": "此操作将永久删除本次会话的「全部对话记录」与「偏好设置」(插件对会话的关联数据除外),且无法恢复。确认继续?"
"title": "确认删除",
"message": "确定要删除此会话的所有自定义规则吗?删除后将恢复使用全局配置。"
},
"batchDeleteConfirm": {
"title": "确认批量删除",
"message": "确定要删除选中的 {count} 条规则吗?删除后将恢复使用全局配置。"
},
"messages": {
"refreshSuccess": "会话列表已刷新",
"personaUpdateSuccess": "人格更新成功",
"personaUpdateError": "人格更新失败",
"providerUpdateSuccess": "Provider 更新成功",
"providerUpdateError": "Provider 更新失败",
"sessionStatusSuccess": "会话 {status}",
"llmStatusSuccess": "LLM {status}",
"ttsStatusSuccess": "TTS {status}",
"statusUpdateError": "状态更新失败",
"loadSessionsError": "加载会话列表失败",
"batchUpdateSuccess": "成功批量更新 {count} 项设置",
"batchUpdatePartial": "批量更新完成,{success} 项成功,{error} 项失败",
"loadPluginsError": "加载插件列表失败",
"pluginStatusSuccess": "插件 {name} {status}",
"pluginStatusError": "插件状态更新失败",
"nameUpdateSuccess": "会话名称更新成功",
"nameUpdateError": "会话名称更新失败",
"deleteSuccess": "会话删除成功",
"deleteError": "会话删除失败"
"refreshSuccess": "数据已刷新",
"loadError": "加载数据失败",
"saveSuccess": "保存成功",
"saveError": "保存失败",
"clearSuccess": "已清除",
"clearError": "清除失败",
"deleteSuccess": "删除成功",
"deleteError": "删除失败",
"noChanges": "没有需要保存的更改",
"batchDeleteSuccess": "批量删除成功",
"batchDeleteError": "批量删除失败"
}
}

View File

@@ -7,6 +7,7 @@ import zhCNActions from './locales/zh-CN/core/actions.json';
import zhCNStatus from './locales/zh-CN/core/status.json';
import zhCNNavigation from './locales/zh-CN/core/navigation.json';
import zhCNHeader from './locales/zh-CN/core/header.json';
import zhCNShared from './locales/zh-CN/core/shared.json';
import zhCNChat from './locales/zh-CN/features/chat.json';
import zhCNExtension from './locales/zh-CN/features/extension.json';
@@ -16,6 +17,7 @@ import zhCNToolUse from './locales/zh-CN/features/tool-use.json';
import zhCNProvider from './locales/zh-CN/features/provider.json';
import zhCNPlatform from './locales/zh-CN/features/platform.json';
import zhCNConfig from './locales/zh-CN/features/config.json';
import zhCNConfigMetadata from './locales/zh-CN/features/config-metadata.json';
import zhCNConsole from './locales/zh-CN/features/console.json';
import zhCNAbout from './locales/zh-CN/features/about.json';
import zhCNSettings from './locales/zh-CN/features/settings.json';
@@ -41,6 +43,7 @@ import enUSActions from './locales/en-US/core/actions.json';
import enUSStatus from './locales/en-US/core/status.json';
import enUSNavigation from './locales/en-US/core/navigation.json';
import enUSHeader from './locales/en-US/core/header.json';
import enUSShared from './locales/en-US/core/shared.json';
import enUSChat from './locales/en-US/features/chat.json';
import enUSExtension from './locales/en-US/features/extension.json';
@@ -50,6 +53,7 @@ import enUSToolUse from './locales/en-US/features/tool-use.json';
import enUSProvider from './locales/en-US/features/provider.json';
import enUSPlatform from './locales/en-US/features/platform.json';
import enUSConfig from './locales/en-US/features/config.json';
import enUSConfigMetadata from './locales/en-US/features/config-metadata.json';
import enUSConsole from './locales/en-US/features/console.json';
import enUSAbout from './locales/en-US/features/about.json';
import enUSSettings from './locales/en-US/features/settings.json';
@@ -77,7 +81,8 @@ export const translations = {
actions: zhCNActions,
status: zhCNStatus,
navigation: zhCNNavigation,
header: zhCNHeader
header: zhCNHeader,
shared: zhCNShared
},
features: {
chat: zhCNChat,
@@ -88,6 +93,7 @@ export const translations = {
provider: zhCNProvider,
platform: zhCNPlatform,
config: zhCNConfig,
'config-metadata': zhCNConfigMetadata,
console: zhCNConsole,
about: zhCNAbout,
settings: zhCNSettings,
@@ -119,7 +125,8 @@ export const translations = {
actions: enUSActions,
status: enUSStatus,
navigation: enUSNavigation,
header: enUSHeader
header: enUSHeader,
shared: enUSShared
},
features: {
chat: enUSChat,
@@ -130,6 +137,7 @@ export const translations = {
provider: enUSProvider,
platform: enUSPlatform,
config: enUSConfig,
'config-metadata': enUSConfigMetadata,
console: enUSConsole,
about: enUSAbout,
settings: enUSSettings,

View File

@@ -69,7 +69,7 @@ const sidebarItem: menu[] = [
},
{
title: 'core.navigation.sessionManagement',
icon: 'mdi-account-group',
icon: 'mdi-pencil-ruler',
to: '/session-management'
},
{

View File

@@ -9,7 +9,7 @@
style="margin-bottom: 16px; align-items: center; gap: 12px; justify-content: space-between; width: 100%;">
<div class="d-flex flex-row align-center" style="gap: 12px;">
<v-select style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
v-if="!isSystemConfig" item-value="id" label="选择配置文件" hide-details density="compact" rounded="md"
v-if="!isSystemConfig" item-value="id" :label="tm('configSelection.selectConfig')" hide-details density="compact" rounded="md"
variant="outlined" @update:model-value="onConfigSelect">
</v-select>
<a style="color: inherit;" href="https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" target="_blank"><v-btn icon="mdi-help-circle" size="small" variant="plain"></v-btn></a>
@@ -19,10 +19,10 @@
<v-btn-toggle v-model="configType" mandatory color="primary" variant="outlined" density="comfortable"
rounded="md" @update:model-value="onConfigTypeToggle">
<v-btn value="normal" prepend-icon="mdi-cog" size="large">
普通
{{ tm('configSelection.normalConfig') }}
</v-btn>
<v-btn value="system" prepend-icon="mdi-cog-outline" size="large">
系统
{{ tm('configSelection.systemConfig') }}
</v-btn>
</v-btn-toggle>
</div>
@@ -45,6 +45,15 @@
@click="configToString(); codeEditorDialog = true">
</v-btn>
<v-tooltip text="测试当前配置" location="left" v-if="!isSystemConfig">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-chat-processing" size="x-large"
style="position: fixed; right: 52px; bottom: 196px;" color="secondary"
@click="openTestChat">
</v-btn>
</template>
</v-tooltip>
</div>
</v-slide-y-transition>
@@ -59,7 +68,7 @@
<v-btn icon @click="codeEditorDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>编辑配置文件</v-toolbar-title>
<v-toolbar-title>{{ tm('codeEditor.title') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items style="display: flex; align-items: center;">
<v-btn style="margin-left: 16px;" size="small" @click="configToString()">{{
@@ -81,15 +90,15 @@
<v-dialog v-model="configManageDialog" max-width="800px">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span class="text-h4">配置文件管理</span>
<span class="text-h4">{{ tm('configManagement.title') }}</span>
<v-btn icon="mdi-close" variant="text" @click="configManageDialog = false"></v-btn>
</v-card-title>
<v-card-text>
<small>AstrBot 支持针对不同机器人分别设置配置文件默认会使用 `default` 配置</small>
<small>{{ tm('configManagement.description') }}</small>
<div class="mt-6 mb-4">
<v-btn prepend-icon="mdi-plus" @click="startCreateConfig" variant="tonal" color="primary">
新建配置文件
{{ tm('configManagement.newConfig') }}
</v-btn>
</div>
@@ -111,18 +120,18 @@
<v-divider v-if="showConfigForm" class="my-6"></v-divider>
<div v-if="showConfigForm">
<h3 class="mb-4">{{ isEditingConfig ? '编辑配置文件' : '新建配置文件' }}</h3>
<h3 class="mb-4">{{ isEditingConfig ? tm('configManagement.editConfig') : tm('configManagement.newConfig') }}</h3>
<h4>名称</h4>
<h4>{{ tm('configManagement.configName') }}</h4>
<v-text-field v-model="configFormData.name" label="填写配置文件名称" variant="outlined" class="mt-4 mb-4"
<v-text-field v-model="configFormData.name" :label="tm('configManagement.fillConfigName')" variant="outlined" class="mt-4 mb-4"
hide-details></v-text-field>
<div class="d-flex justify-end mt-4" style="gap: 8px;">
<v-btn variant="text" @click="cancelConfigForm">取消</v-btn>
<v-btn variant="text" @click="cancelConfigForm">{{ tm('buttons.cancel') }}</v-btn>
<v-btn color="primary" @click="saveConfigForm"
:disabled="!configFormData.name">
{{ isEditingConfig ? '更新' : '创建' }}
{{ isEditingConfig ? tm('buttons.update') : tm('buttons.create') }}
</v-btn>
</div>
</div>
@@ -135,6 +144,34 @@
</v-snackbar>
<WaitingForRestart ref="wfr"></WaitingForRestart>
<!-- 测试聊天抽屉 -->
<v-overlay
v-model="testChatDrawer"
class="test-chat-overlay"
location="right"
transition="slide-x-reverse-transition"
:scrim="true"
@click:outside="closeTestChat"
>
<v-card class="test-chat-card" elevation="12">
<div class="test-chat-header">
<div>
<span class="text-h6">测试配置</span>
<div v-if="selectedConfigInfo.name" class="text-caption text-grey">
{{ selectedConfigInfo.name }} ({{ testConfigId }})
</div>
</div>
<v-btn icon variant="text" @click="closeTestChat">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<v-divider></v-divider>
<div class="test-chat-content">
<StandaloneChat v-if="testChatDrawer" :configId="testConfigId" />
</div>
</v-card>
</v-overlay>
</template>
@@ -142,6 +179,7 @@
import axios from 'axios';
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { useI18n, useModuleI18n } from '@/i18n/composables';
@@ -150,7 +188,8 @@ export default {
components: {
AstrBotCoreConfigWrapper,
VueMonacoEditor,
WaitingForRestart
WaitingForRestart,
StandaloneChat
},
props: {
initialConfigId: {
@@ -188,7 +227,7 @@ export default {
const items = [...this.configInfoList];
items.push({
id: '_%manage%_',
name: '管理配置文件...',
name: this.tm('configManagement.manageConfigs'),
umop: []
});
return items;
@@ -238,6 +277,10 @@ export default {
name: '',
},
editingConfigId: null,
// 测试聊天
testChatDrawer: false,
testConfigId: null,
}
},
mounted() {
@@ -367,7 +410,7 @@ export default {
}
}).catch((err) => {
console.error(err);
this.save_message = "新配置文件创建失败";
this.save_message = this.tm('configManagement.createFailed');
this.save_message_snack = true;
this.save_message_success = "error";
});
@@ -410,7 +453,7 @@ export default {
},
saveConfigForm() {
if (!this.configFormData.name) {
this.save_message = "请填写配置名称";
this.save_message = this.tm('configManagement.pleaseEnterName');
this.save_message_snack = true;
this.save_message_success = "error";
return;
@@ -423,7 +466,7 @@ export default {
}
},
confirmDeleteConfig(config) {
if (confirm(`确定要删除配置文件 "${config.name}" 吗?此操作不可恢复。`)) {
if (confirm(this.tm('configManagement.confirmDelete').replace('{name}', config.name))) {
this.deleteConfig(config.id);
}
},
@@ -445,7 +488,7 @@ export default {
}
}).catch((err) => {
console.error(err);
this.save_message = "删除配置文件失败";
this.save_message = this.tm('configManagement.deleteFailed');
this.save_message_snack = true;
this.save_message_success = "error";
});
@@ -468,7 +511,7 @@ export default {
}
}).catch((err) => {
console.error(err);
this.save_message = "更新配置文件失败";
this.save_message = this.tm('configManagement.updateFailed');
this.save_message_snack = true;
this.save_message_success = "error";
});
@@ -506,6 +549,20 @@ export default {
this.getConfigInfoList("default");
}
}
},
openTestChat() {
if (!this.selectedConfigID) {
this.save_message = "请先选择一个配置文件";
this.save_message_snack = true;
this.save_message_success = "warning";
return;
}
this.testConfigId = this.selectedConfigID;
this.testChatDrawer = true;
},
closeTestChat() {
this.testChatDrawer = false;
this.testConfigId = null;
}
},
}
@@ -565,4 +622,32 @@ export default {
width: 100%;
}
}
/* 测试聊天抽屉样式 */
.test-chat-overlay {
align-items: stretch;
justify-content: flex-end;
}
.test-chat-card {
width: clamp(320px, 50vw, 720px);
height: calc(100vh - 32px);
display: flex;
flex-direction: column;
margin: 16px;
}
.test-chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px 20px;
}
.test-chat-content {
flex: 1;
overflow: hidden;
padding: 0;
border-radius: 0 0 16px 16px;
}
</style>

View File

@@ -9,6 +9,7 @@ import axios from 'axios';
import { pinyin } from 'pinyin-pro';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import defaultPluginIcon from '@/assets/images/plugin_icon.png';
import { ref, computed, onMounted, reactive, inject, watch } from 'vue';
@@ -41,6 +42,7 @@ const loadingDialog = reactive({
const showPluginInfoDialog = ref(false);
const selectedPlugin = ref({});
const curr_namespace = ref("");
const updatingAll = ref(false);
const readmeDialog = reactive({
show: false,
@@ -225,6 +227,10 @@ const paginatedPlugins = computed(() => {
return sortedPlugins.value.slice(start, end);
});
const updatableExtensions = computed(() => {
return extension_data?.data?.filter(ext => ext.has_update) || [];
});
// 方法
const toggleShowReserved = () => {
showReserved.value = !showReserved.value;
@@ -371,6 +377,56 @@ const updateExtension = async (extension_name) => {
}
};
const updateAllExtensions = async () => {
if (updatingAll.value || updatableExtensions.value.length === 0) return;
updatingAll.value = true;
loadingDialog.title = tm('status.loading');
loadingDialog.statusCode = 0;
loadingDialog.result = "";
loadingDialog.show = true;
const targets = updatableExtensions.value.map(ext => ext.name);
try {
const res = await axios.post('/api/plugin/update-all', {
names: targets,
proxy: localStorage.getItem('selectedGitHubProxy') || ""
});
if (res.data.status === "error") {
onLoadingDialogResult(2, res.data.message || tm('messages.updateAllFailed', {
failed: targets.length,
total: targets.length
}), -1);
return;
}
const results = res.data.data?.results || [];
const failures = results.filter(r => r.status !== 'ok');
try {
await getExtensions();
} catch (err) {
const errorMsg = err.response?.data?.message || err.message || String(err);
failures.push({ name: 'refresh', status: 'error', message: errorMsg });
}
if (failures.length === 0) {
onLoadingDialogResult(1, tm('messages.updateAllSuccess'));
} else {
const failureText = tm('messages.updateAllFailed', {
failed: failures.length,
total: targets.length
});
const detail = failures.map(f => `${f.name}: ${f.message}`).join('\n');
onLoadingDialogResult(2, `${failureText}\n${detail}`, -1);
}
} catch (err) {
const errorMsg = err.response?.data?.message || err.message || String(err);
onLoadingDialogResult(2, errorMsg, -1);
} finally {
updatingAll.value = false;
}
};
const pluginOn = async (extension) => {
try {
const res = await axios.post('/api/plugin/on', { name: extension.name });
@@ -719,6 +775,12 @@ watch(marketSearch, (newVal) => {
{{ showReserved ? tm('buttons.hideSystemPlugins') : tm('buttons.showSystemPlugins') }}
</v-btn>
<v-btn class="ml-2" color="warning" variant="tonal" :disabled="updatableExtensions.length === 0"
:loading="updatingAll" @click="updateAllExtensions">
<v-icon>mdi-update</v-icon>
{{ tm('buttons.updateAll') }}
</v-btn>
<v-btn class="ml-2" color="primary" variant="tonal" @click="dialog = true">
<v-icon>mdi-plus</v-icon>
{{ tm('buttons.install') }}
@@ -939,7 +1001,7 @@ watch(marketSearch, (newVal) => {
<v-row style="min-height: 26rem;">
<v-col v-for="plugin in paginatedPlugins" :key="plugin.name" cols="12" md="6" lg="4">
<v-card class="rounded-lg d-flex flex-column" elevation="0"
<v-card class="rounded-lg d-flex flex-column plugin-card" elevation="0"
style=" height: 12rem; position: relative;">
<!-- 推荐标记 -->
@@ -950,8 +1012,8 @@ watch(marketSearch, (newVal) => {
<v-card-text
style="padding: 12px; padding-bottom: 8px; display: flex; gap: 12px; width: 100%; flex: 1; overflow: hidden;">
<div v-if="plugin?.logo" style="flex-shrink: 0;">
<img :src="plugin.logo" :alt="plugin.name"
<div style="flex-shrink: 0;">
<img :src="plugin?.logo || defaultPluginIcon" :alt="plugin.name"
style="height: 75px; width: 75px; border-radius: 8px; object-fit: cover;" />
</div>
@@ -986,8 +1048,7 @@ watch(marketSearch, (newVal) => {
</div>
<!-- Description -->
<div class="text-caption"
style="overflow: scroll; color: rgba(var(--v-theme-on-surface), 0.6); line-height: 1.3; margin-bottom: 6px; flex: 1;">
<div class="text-caption plugin-description">
{{ plugin.desc }}
</div>
@@ -1246,4 +1307,36 @@ watch(marketSearch, (newVal) => {
border-radius: 5px;
background-color: #f5f5f5;
}
.plugin-description {
color: rgba(var(--v-theme-on-surface), 0.6);
line-height: 1.3;
margin-bottom: 6px;
flex: 1;
overflow-y: hidden;
}
.plugin-card:hover .plugin-description {
overflow-y: auto;
}
.plugin-description::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.plugin-description::-webkit-scrollbar-track {
background: transparent;
}
.plugin-description::-webkit-scrollbar-thumb {
background-color: rgba(var(--v-theme-primary-rgb), 0.4);
border-radius: 4px;
border: 2px solid transparent;
background-clip: content-box;
}
.plugin-description::-webkit-scrollbar-thumb:hover {
background-color: rgba(var(--v-theme-primary-rgb), 0.6);
}
</style>

View File

@@ -35,7 +35,7 @@
</div>
<!-- 日志部分 -->
<v-card elevation="0" class="mt-4">
<v-card elevation="0" class="mt-4 mb-10">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon class="me-2">mdi-console-line</v-icon>
<span class="text-h4">{{ tm('logs.title') }}</span>
@@ -233,5 +233,6 @@ export default {
.platform-page {
padding: 20px;
padding-top: 8px;
padding-bottom: 40px;
}
</style>

View File

@@ -69,6 +69,25 @@
:loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange"
:bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider"
@copy="copyProvider" :show-copy-button="true">
<template #item-details="{ item }">
<!-- 测试状态 chip -->
<v-tooltip v-if="getProviderStatus(item.id)" location="top" max-width="300">
<template v-slot:activator="{ props }">
<v-chip v-bind="props" :color="getStatusColor(getProviderStatus(item.id).status)" size="small">
<v-icon start size="small">
{{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
'mdi-clock-outline' }}
</v-icon>
{{ getStatusText(getProviderStatus(item.id).status) }}
</v-chip>
</template>
<span v-if="getProviderStatus(item.id).status === 'unavailable'">
{{ getProviderStatus(item.id).error }}
</span>
<span v-else>{{ getStatusText(getProviderStatus(item.id).status) }}</span>
</v-tooltip>
</template>
<template #actions="{ item }">
<v-btn style="z-index: 100000;" variant="tonal" color="info" rounded="xl" size="small"
:loading="isProviderTesting(item.id)" @click="testSingleProvider(item)">
@@ -96,75 +115,40 @@
:loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange"
:bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider"
@copy="copyProvider" :show-copy-button="true">
<template #item-details="{ item }">
<!-- 测试状态 chip -->
<v-tooltip v-if="getProviderStatus(item.id)" location="top" max-width="300">
<template v-slot:activator="{ props }">
<v-chip v-bind="props" :color="getStatusColor(getProviderStatus(item.id).status)" size="small">
<v-icon start size="small">
{{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
'mdi-clock-outline' }}
</v-icon>
{{ getStatusText(getProviderStatus(item.id).status) }}
</v-chip>
</template>
<span v-if="getProviderStatus(item.id).status === 'unavailable'">
{{ getProviderStatus(item.id).error }}
</span>
<span v-else>{{ getStatusText(getProviderStatus(item.id).status) }}</span>
</v-tooltip>
</template>
<template #actions="{ item }">
<v-btn style="z-index: 100000;" variant="tonal" color="info" rounded="xl" size="small"
:loading="isProviderTesting(item.id)" @click="testSingleProvider(item)">
{{ tm('availability.test') }}
</v-btn>
</template>
<template v-slot:details="{ item }">
</template>
</item-card>
</v-col>
</v-row>
</template>
</div>
<!-- 供应商状态部分 -->
<v-card elevation="0" class="mt-4">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon class="me-2">mdi-heart-pulse</v-icon>
<span class="text-h4">{{ tm('availability.title') }}</span>
<v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" :loading="testingProviders.length > 0" @click="fetchProviderStatus">
<v-icon left>mdi-refresh</v-icon>
{{ tm('availability.refresh') }}
</v-btn>
<v-btn variant="text" color="primary" @click="showStatus = !showStatus" style="margin-left: 8px;">
{{ showStatus ? tm('logs.collapse') : tm('logs.expand') }}
<v-icon>{{ showStatus ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</v-card-title>
<v-expand-transition>
<v-card-text class="pa-0" v-if="showStatus">
<v-card-text class="px-4 py-3">
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
{{ tm('availability.noData') }}
</v-alert>
<v-container v-else class="pa-0">
<v-row>
<v-col v-for="status in providerStatuses" :key="status.id" cols="12" sm="6" md="4">
<v-card variant="outlined" class="status-card" :class="`status-${status.status}`">
<v-card-item>
<v-icon v-if="status.status === 'available'" color="success"
class="me-2">mdi-check-circle</v-icon>
<v-icon v-else-if="status.status === 'unavailable'" color="error"
class="me-2">mdi-alert-circle</v-icon>
<v-progress-circular v-else-if="status.status === 'pending'" indeterminate color="primary"
size="20" width="2" class="me-2"></v-progress-circular>
<span class="font-weight-bold">{{ status.id }}</span>
<v-chip :color="getStatusColor(status.status)" size="small" class="ml-2">
{{ getStatusText(status.status) }}
</v-chip>
</v-card-item>
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
<span class="font-weight-bold">{{ tm('availability.errorMessage') }}:</span> {{ status.error }}
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card-text>
</v-expand-transition>
</v-card>
<!-- 日志部分 -->
<v-card elevation="0" class="mt-4">
<v-card elevation="0" class="mt-4 mb-10">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon class="me-2">mdi-console-line</v-icon>
<span class="text-h4">{{ tm('logs.title') }}</span>
@@ -751,11 +735,14 @@ export default {
return this.testingProviders.includes(providerId);
},
getProviderStatus(providerId) {
return this.providerStatuses.find(s => s.id === providerId);
},
async testSingleProvider(provider) {
if (this.isProviderTesting(provider.id)) return;
this.testingProviders.push(provider.id);
this.showStatus = true; // 自动展开状态部分
// 更新UI为pending状态
const statusIndex = this.providerStatuses.findIndex(s => s.id === provider.id);
@@ -862,6 +849,7 @@ export default {
.provider-page {
padding: 20px;
padding-top: 8px;
padding-bottom: 40px;
}
.status-card {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import builtins
from astrbot.api import star
from astrbot.api import sp, star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
@@ -17,6 +17,13 @@ class PersonaCommands:
default_persona = await self.context.persona_manager.get_default_persona_v3(
umo=umo,
)
force_applied_persona_id = (
await sp.get_async(
scope="umo", scope_id=umo, key="session_service_config", default={}
)
).get("persona_id")
curr_cid_title = ""
if cid:
conv = await self.context.conversation_manager.get_conversation(
@@ -36,6 +43,9 @@ class PersonaCommands:
else:
curr_persona_name = conv.persona_id
if force_applied_persona_id:
curr_persona_name = f"{curr_persona_name} (自定义规则)"
curr_cid_title = conv.title if conv.title else "新对话"
curr_cid_title += f"({cid[:4]})"
@@ -113,9 +123,15 @@ class PersonaCommands:
message.unified_msg_origin,
ps,
)
force_warn_msg = ""
if force_applied_persona_id:
force_warn_msg = (
"提醒:由于自定义规则,您现在切换的人格将不会生效。"
)
message.set_result(
MessageEventResult().message(
"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。",
f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}",
),
)
else:

View File

@@ -1,5 +1,7 @@
import asyncio
import re
from astrbot import logger
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.core.provider.entities import ProviderType
@@ -9,6 +11,39 @@ class ProviderCommands:
def __init__(self, context: star.Context):
self.context = context
def _log_reachability_failure(
self,
provider,
provider_capability_type: ProviderType | None,
err_code: str,
err_reason: str,
):
"""记录不可达原因到日志。"""
meta = provider.meta()
logger.warning(
"Provider reachability check failed: id=%s type=%s code=%s reason=%s",
meta.id,
provider_capability_type.name if provider_capability_type else "unknown",
err_code,
err_reason,
)
async def _test_provider_capability(self, provider):
"""测试单个 provider 的可用性"""
meta = provider.meta()
provider_capability_type = meta.provider_type
try:
await provider.test()
return True, None, None
except Exception as e:
err_code = "TEST_FAILED"
err_reason = str(e)
self._log_reachability_failure(
provider, provider_capability_type, err_code, err_reason
)
return False, err_code, err_reason
async def provider(
self,
event: AstrMessageEvent,
@@ -17,46 +52,131 @@ class ProviderCommands:
):
"""查看或者切换 LLM Provider"""
umo = event.unified_msg_origin
cfg = self.context.get_config(umo).get("provider_settings", {})
reachability_check_enabled = cfg.get("reachability_check", True)
if idx is None:
parts = ["## 载入的 LLM 提供商\n"]
for idx, llm in enumerate(self.context.get_all_providers()):
id_ = llm.meta().id
line = f"{idx + 1}. {id_} ({llm.meta().model})"
# 获取所有类型的提供商
llms = list(self.context.get_all_providers())
ttss = self.context.get_all_tts_providers()
stts = self.context.get_all_stt_providers()
# 构造待检测列表: [(provider, type_label), ...]
all_providers = []
all_providers.extend([(p, "llm") for p in llms])
all_providers.extend([(p, "tts") for p in ttss])
all_providers.extend([(p, "stt") for p in stts])
# 并发测试连通性
if reachability_check_enabled:
if all_providers:
await event.send(
MessageEventResult().message(
"正在进行提供商可达性测试,请稍候..."
)
)
check_results = await asyncio.gather(
*[self._test_provider_capability(p) for p, _ in all_providers],
return_exceptions=True,
)
else:
# 用 None 表示未检测
check_results = [None for _ in all_providers]
# 整合结果
display_data = []
for (p, p_type), reachable in zip(all_providers, check_results):
meta = p.meta()
id_ = meta.id
error_code = None
if isinstance(reachable, Exception):
# 异常情况下兜底处理,避免单个 provider 导致列表失败
self._log_reachability_failure(
p,
None,
reachable.__class__.__name__,
str(reachable),
)
reachable_flag = False
error_code = reachable.__class__.__name__
elif isinstance(reachable, tuple):
reachable_flag, error_code, _ = reachable
else:
reachable_flag = reachable
# 根据类型构建显示名称
if p_type == "llm":
info = f"{id_} ({meta.model})"
else:
info = f"{id_}"
# 确定状态标记
if reachable_flag is True:
mark = ""
elif reachable_flag is False:
if error_code:
mark = f" ❌(错误码: {error_code})"
else:
mark = ""
else:
mark = "" # 不支持检测时不显示标记
display_data.append(
{
"type": p_type,
"info": info,
"mark": mark,
"provider": p,
}
)
# 分组输出
# 1. LLM
llm_data = [d for d in display_data if d["type"] == "llm"]
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
provider_using = self.context.get_using_provider(umo=umo)
if provider_using and provider_using.meta().id == id_:
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " (当前使用)"
parts.append(line + "\n")
tts_providers = self.context.get_all_tts_providers()
if tts_providers:
# 2. TTS
tts_data = [d for d in display_data if d["type"] == "tts"]
if tts_data:
parts.append("\n## 载入的 TTS 提供商\n")
for idx, tts in enumerate(tts_providers):
id_ = tts.meta().id
line = f"{idx + 1}. {id_}"
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
tts_using = self.context.get_using_tts_provider(umo=umo)
if tts_using and tts_using.meta().id == id_:
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
stt_providers = self.context.get_all_stt_providers()
if stt_providers:
# 3. STT
stt_data = [d for d in display_data if d["type"] == "stt"]
if stt_data:
parts.append("\n## 载入的 STT 提供商\n")
for idx, stt in enumerate(stt_providers):
id_ = stt.meta().id
line = f"{idx + 1}. {id_}"
for i, d in enumerate(stt_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
stt_using = self.context.get_using_stt_provider(umo=umo)
if stt_using and stt_using.meta().id == id_:
if stt_using and stt_using.meta().id == d["provider"].meta().id:
line += " (当前使用)"
parts.append(line + "\n")
parts.append("\n使用 /provider <序号> 切换 LLM 提供商。")
ret = "".join(parts)
if tts_providers:
if ttss:
ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。"
if stt_providers:
ret += "\n使用 /provider stt <切换> STT 提供商。"
if stts:
ret += "\n使用 /provider stt <序号> 切换 STT 提供商。"
if not reachability_check_enabled:
ret += "\n已跳过提供商可达性检测,如需检测请在配置文件中开启。"
event.set_result(MessageEventResult().message(ret))
elif idx == "tts":

View File

@@ -6,9 +6,9 @@ from collections import defaultdict
from astrbot import logger
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent
from astrbot.api.message_components import Image, Plain
from astrbot.api.message_components import At, Image, Plain
from astrbot.api.platform import MessageType
from astrbot.api.provider import Provider, ProviderRequest
from astrbot.api.provider import LLMResponse, Provider, ProviderRequest
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
"""
@@ -30,16 +30,13 @@ class LongTermMemory:
except BaseException as e:
logger.error(e)
max_cnt = 300
image_caption = (
True
if cfg["provider_settings"]["default_image_caption_provider_id"]
and cfg["provider_ltm_settings"]["image_caption"]
else False
)
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
image_caption_provider_id = cfg["provider_settings"][
"default_image_caption_provider_id"
]
image_caption_provider_id = cfg["provider_ltm_settings"].get(
"image_caption_provider_id"
)
image_caption = cfg["provider_ltm_settings"]["image_caption"] and bool(
image_caption_provider_id
)
active_reply = cfg["provider_ltm_settings"]["active_reply"]
enable_active_reply = active_reply.get("enable", False)
ar_method = active_reply["method"]
@@ -142,6 +139,8 @@ class LongTermMemory:
logger.error(f"获取图片描述失败: {e}")
else:
parts.append(" [Image]")
elif isinstance(comp, At):
parts.append(f" [At: {comp.name}]")
final_message = "".join(parts)
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
@@ -159,8 +158,12 @@ class LongTermMemory:
cfg = self.cfg(event)
if cfg["enable_active_reply"]:
prompt = req.prompt
req.prompt = f"You are now in a chatroom. The chat history is as follows:\n{chats_str}"
req.prompt += f"\nNow, a new message is coming: `{prompt}`. Please react to it. Only output your response and do not output any other information."
req.prompt = (
f"You are now in a chatroom. The chat history is as follows:\n{chats_str}"
f"\nNow, a new message is coming: `{prompt}`. "
"Please react to it. Only output your response and do not output any other information. "
"You MUST use the SAME language as the chatroom is using."
)
req.contexts = [] # 清空上下文当使用了主动回复所有聊天记录都在一个prompt中。
else:
req.system_prompt += (
@@ -168,13 +171,15 @@ class LongTermMemory:
)
req.system_prompt += chats_str
async def after_req_llm(self, event: AstrMessageEvent):
async def after_req_llm(self, event: AstrMessageEvent, llm_resp: LLMResponse):
if event.unified_msg_origin not in self.session_chats:
return
if event.get_result() and event.get_result().is_llm_result():
final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {event.get_result().get_plain_text()}"
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
if llm_resp.completion_text:
final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {llm_resp.completion_text}"
logger.debug(
f"Recorded AI response: {event.unified_msg_origin} | {final_message}"
)
self.session_chats[event.unified_msg_origin].append(final_message)
cfg = self.cfg(event)
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:

View File

@@ -322,7 +322,7 @@ class Main(star.Star):
@filter.on_llm_response()
async def inject_reasoning(self, event: AstrMessageEvent, resp: LLMResponse):
"""在 LLM 响应后基于配置注入思考过程文本"""
"""在 LLM 响应后基于配置注入思考过程文本 / 在 LLM 响应后记录对话"""
umo = event.unified_msg_origin
cfg = self.context.get_config(umo).get("provider_settings", {})
show_reasoning = cfg.get("display_reasoning_text", False)
@@ -331,12 +331,9 @@ class Main(star.Star):
f"🤔 思考: {resp.reasoning_content}\n\n{resp.completion_text}"
)
@filter.after_message_sent()
async def after_llm_req(self, event: AstrMessageEvent):
"""在 LLM 请求后记录对话"""
if self.ltm and self.ltm_enabled(event):
try:
await self.ltm.after_req_llm(event)
await self.ltm.after_req_llm(event, resp)
except Exception as e:
logger.error(f"ltm: {e}")

View File

@@ -3,7 +3,7 @@ import copy
import datetime
import zoneinfo
from astrbot.api import logger, star
from astrbot.api import logger, sp, star
from astrbot.api.event import AstrMessageEvent
from astrbot.api.message_components import Image, Reply
from astrbot.api.provider import Provider, ProviderRequest
@@ -21,16 +21,26 @@ class ProcessLLMRequest:
else:
logger.info(f"Timezone set to: {self.timezone}")
def _ensure_persona(self, req: ProviderRequest, cfg: dict):
async def _ensure_persona(self, req: ProviderRequest, cfg: dict, umo: str):
"""确保用户人格已加载"""
if not req.conversation:
return
# persona inject
persona_id = req.conversation.persona_id or cfg.get("default_personality")
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
default_persona = self.ctx.persona_manager.selected_default_persona_v3
if default_persona:
persona_id = default_persona["name"]
# custom rule is preferred
persona_id = (
await sp.get_async(
scope="umo", scope_id=umo, key="session_service_config", default={}
)
).get("persona_id")
if not persona_id:
persona_id = req.conversation.persona_id or cfg.get("default_personality")
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
default_persona = self.ctx.persona_manager.selected_default_persona_v3
if default_persona:
persona_id = default_persona["name"]
persona = next(
builtins.filter(
lambda persona: persona["name"] == persona_id,
@@ -152,7 +162,7 @@ class ProcessLLMRequest:
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
if req.conversation:
# inject persona for this request
self._ensure_persona(req, cfg)
await self._ensure_persona(req, cfg, event.unified_msg_origin)
# image caption
if img_cap_prov_id and req.image_urls:

View File

@@ -14,7 +14,7 @@ from astrbot.core.utils.session_waiter import (
)
class Waiter(Star):
class Main(Star):
"""会话控制"""
def __init__(self, context: Context):

View File

@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.6.1"
version = "4.7.4"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
requires-python = ">=3.10"

View File

@@ -21,7 +21,17 @@ async def core_lifecycle_td(tmp_path_factory):
log_broker = LogBroker()
core_lifecycle = AstrBotCoreLifecycle(log_broker, db)
await core_lifecycle.initialize()
return core_lifecycle
try:
yield core_lifecycle
finally:
# 优先停止核心生命周期以释放资源(包括关闭 MCP 等后台任务)
try:
_stop_res = core_lifecycle.stop()
if asyncio.iscoroutine(_stop_res):
await _stop_res
except Exception:
# 停止过程中如有异常,不影响后续清理
pass
@pytest.fixture(scope="module")

View File

@@ -39,6 +39,7 @@ def plugin_manager_pm(tmp_path):
message_history_manager = MagicMock()
persona_manager = MagicMock()
astrbot_config_mgr = MagicMock()
knowledge_base_manager = MagicMock()
star_context = Context(
event_queue,
@@ -50,6 +51,7 @@ def plugin_manager_pm(tmp_path):
message_history_manager,
persona_manager,
astrbot_config_mgr,
knowledge_base_manager=knowledge_base_manager,
)
# Create the PluginManager instance