Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 464882f206 | |||
| 6736fb85c2 | |||
| 1f75255950 | |||
| a954e75547 | |||
| d2b9997620 | |||
| 36432c4361 | |||
| 36f0d1f0f9 | |||
| f65b268bb2 | |||
| fe06dfcca3 | |||
| bc9043bc3f | |||
| 430694aae9 |
@@ -67,8 +67,6 @@ AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
陪伴与能力**从来不应该是**对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人——致敬[ATRI](https://zh.wikipedia.org/zh-cn/ATRI_-My_Dear_Moments-)。
|
||||
|
||||
## 快速开始
|
||||
|
||||
#### Docker 部署(推荐 🥳)
|
||||
@@ -268,6 +266,6 @@ pre-commit install
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
</div
|
||||
陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.14.1"
|
||||
__version__ = "4.14.4"
|
||||
|
||||
@@ -213,6 +213,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
if not llm_response.is_chunk and llm_response.usage:
|
||||
# only count the token usage of the final response for computation purpose
|
||||
self.stats.token_usage += llm_response.usage
|
||||
if self.req.conversation:
|
||||
self.req.conversation.token_usage = llm_response.usage.total
|
||||
break # got final response
|
||||
|
||||
if not llm_resp_result:
|
||||
|
||||
@@ -7,6 +7,7 @@ import datetime
|
||||
import json
|
||||
import os
|
||||
import zoneinfo
|
||||
from collections.abc import Coroutine
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from astrbot.api import sp
|
||||
@@ -114,6 +115,7 @@ class MainAgentBuildResult:
|
||||
agent_runner: AgentRunner
|
||||
provider_request: ProviderRequest
|
||||
provider: Provider
|
||||
reset_coro: Coroutine | None = None
|
||||
|
||||
|
||||
def _select_provider(
|
||||
@@ -830,38 +832,6 @@ def _get_compress_provider(
|
||||
return provider
|
||||
|
||||
|
||||
def _apply_global_context_info(event: AstrMessageEvent, req: ProviderRequest) -> None:
|
||||
"""Add platform and session information to user prompt when in global unified context mode."""
|
||||
from astrbot.core.config.default import (
|
||||
GLOBAL_UNIFIED_CONTEXT_UMO,
|
||||
ORIGINAL_UMO_KEY,
|
||||
)
|
||||
|
||||
if event.unified_msg_origin != GLOBAL_UNIFIED_CONTEXT_UMO:
|
||||
return
|
||||
|
||||
# Get original UMO from extras
|
||||
original_umo = event.get_extra(ORIGINAL_UMO_KEY)
|
||||
if not original_umo:
|
||||
return
|
||||
|
||||
# Parse the original UMO to extract platform, message type, and session info
|
||||
try:
|
||||
parts = original_umo.split(":", 2)
|
||||
if len(parts) != 3:
|
||||
logger.warning(
|
||||
f"Original UMO format is invalid (expected 3 parts): {original_umo}"
|
||||
)
|
||||
return
|
||||
|
||||
platform_id, message_type, session_id = parts
|
||||
context_info = f"[Context: Platform={platform_id}, Type={message_type}, Session={session_id}]"
|
||||
# Prepend context info to the user prompt
|
||||
req.prompt = f"{context_info} {req.prompt or ''}"
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse original UMO for global context: {e}")
|
||||
|
||||
|
||||
async def build_main_agent(
|
||||
*,
|
||||
event: AstrMessageEvent,
|
||||
@@ -869,8 +839,12 @@ async def build_main_agent(
|
||||
config: MainAgentBuildConfig,
|
||||
provider: Provider | None = None,
|
||||
req: ProviderRequest | None = None,
|
||||
apply_reset: bool = True,
|
||||
) -> MainAgentBuildResult | None:
|
||||
"""构建主对话代理(Main Agent),并且自动 reset。"""
|
||||
"""构建主对话代理(Main Agent),并且自动 reset。
|
||||
|
||||
If apply_reset is False, will not call reset on the agent runner.
|
||||
"""
|
||||
provider = provider or _select_provider(event, plugin_context)
|
||||
if provider is None:
|
||||
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
|
||||
@@ -920,9 +894,6 @@ async def build_main_agent(
|
||||
if isinstance(req.contexts, str):
|
||||
req.contexts = json.loads(req.contexts)
|
||||
|
||||
# Apply global context information if enabled
|
||||
_apply_global_context_info(event, req)
|
||||
|
||||
if config.file_extract_enabled:
|
||||
try:
|
||||
await _apply_file_extract(event, req, config)
|
||||
@@ -990,7 +961,7 @@ async def build_main_agent(
|
||||
if action_type == "live":
|
||||
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
|
||||
|
||||
await agent_runner.reset(
|
||||
reset_coro = agent_runner.reset(
|
||||
provider=provider,
|
||||
request=req,
|
||||
run_context=AgentContextWrapper(
|
||||
@@ -1008,8 +979,12 @@ async def build_main_agent(
|
||||
tool_schema_mode=config.tool_schema_mode,
|
||||
)
|
||||
|
||||
if apply_reset:
|
||||
await reset_coro
|
||||
|
||||
return MainAgentBuildResult(
|
||||
agent_runner=agent_runner,
|
||||
provider_request=req,
|
||||
provider=provider,
|
||||
reset_coro=reset_coro if not apply_reset else None,
|
||||
)
|
||||
|
||||
@@ -212,10 +212,6 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
||||
"required": ["type"],
|
||||
},
|
||||
},
|
||||
"session": {
|
||||
"type": "string",
|
||||
"description": "Target session ID in format 'platform:type:session_id'. If not specified, sends to the current session.",
|
||||
},
|
||||
},
|
||||
"required": ["messages"],
|
||||
}
|
||||
@@ -257,12 +253,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> ToolExecResult:
|
||||
# In global context mode, default to original UMO if session not specified
|
||||
from astrbot.core.config.default import ORIGINAL_UMO_KEY
|
||||
|
||||
original_umo = context.context.event.get_extra(ORIGINAL_UMO_KEY)
|
||||
default_session = original_umo or context.context.event.unified_msg_origin
|
||||
session = kwargs.get("session") or default_session
|
||||
session = kwargs.get("session") or context.context.event.unified_msg_origin
|
||||
messages = kwargs.get("messages")
|
||||
|
||||
if not isinstance(messages, list) or not messages:
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.14.1"
|
||||
VERSION = "4.14.4"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
@@ -17,17 +17,11 @@ WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
"lark",
|
||||
]
|
||||
|
||||
# Constant UMO for global unified context mode
|
||||
GLOBAL_UNIFIED_CONTEXT_UMO = "global::global"
|
||||
# Key for storing original UMO in event extras when global context mode is enabled
|
||||
ORIGINAL_UMO_KEY = "original_umo"
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"config_version": 2,
|
||||
"platform_settings": {
|
||||
"unique_session": False,
|
||||
"global_unified_context_mode": False,
|
||||
"rate_limit": {
|
||||
"time": 60,
|
||||
"count": 30,
|
||||
|
||||
@@ -164,6 +164,7 @@ class InternalAgentSubStage(Stage):
|
||||
event=event,
|
||||
plugin_context=self.ctx.plugin_manager.context,
|
||||
config=build_cfg,
|
||||
apply_reset=False,
|
||||
)
|
||||
|
||||
if build_result is None:
|
||||
@@ -172,6 +173,7 @@ class InternalAgentSubStage(Stage):
|
||||
agent_runner = build_result.agent_runner
|
||||
req = build_result.provider_request
|
||||
provider = build_result.provider
|
||||
reset_coro = build_result.reset_coro
|
||||
|
||||
api_base = provider.provider_config.get("api_base", "")
|
||||
for host in decoded_blocked:
|
||||
@@ -190,6 +192,10 @@ class InternalAgentSubStage(Stage):
|
||||
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
|
||||
return
|
||||
|
||||
# apply reset
|
||||
if reset_coro:
|
||||
await reset_coro
|
||||
|
||||
action_type = event.get_extra("action_type")
|
||||
|
||||
event.trace.record(
|
||||
@@ -357,7 +363,8 @@ class InternalAgentSubStage(Stage):
|
||||
|
||||
token_usage = None
|
||||
if runner_stats:
|
||||
token_usage = runner_stats.token_usage.total
|
||||
# token_usage = runner_stats.token_usage.total
|
||||
token_usage = llm_response.usage.total if llm_response.usage else None
|
||||
|
||||
await self.conv_manager.update_conversation(
|
||||
event.unified_msg_origin,
|
||||
|
||||
@@ -169,16 +169,6 @@ class RespondStage(Stage):
|
||||
f"Prepare to send - {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}",
|
||||
)
|
||||
|
||||
# Restore original UMO before sending if in global context mode
|
||||
from astrbot.core.config.default import ORIGINAL_UMO_KEY
|
||||
|
||||
original_umo = event.get_extra(ORIGINAL_UMO_KEY)
|
||||
if original_umo:
|
||||
logger.debug(
|
||||
f"Restoring original UMO before sending: {event.unified_msg_origin} -> {original_umo}"
|
||||
)
|
||||
event.unified_msg_origin = original_umo
|
||||
|
||||
if result.result_content_type == ResultContentType.STREAMING_RESULT:
|
||||
if result.async_stream is None:
|
||||
logger.warning("async_stream 为空,跳过发送。")
|
||||
|
||||
@@ -72,29 +72,11 @@ class WakingCheckStage(Stage):
|
||||
)
|
||||
platform_settings = self.ctx.astrbot_config.get("platform_settings", {})
|
||||
self.unique_session = platform_settings.get("unique_session", False)
|
||||
self.global_unified_context_mode = platform_settings.get(
|
||||
"global_unified_context_mode", False
|
||||
)
|
||||
|
||||
async def process(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
) -> None | AsyncGenerator[None, None]:
|
||||
# apply global unified context mode
|
||||
if self.global_unified_context_mode:
|
||||
from astrbot.core.config.default import (
|
||||
GLOBAL_UNIFIED_CONTEXT_UMO,
|
||||
ORIGINAL_UMO_KEY,
|
||||
)
|
||||
|
||||
original_umo = event.unified_msg_origin
|
||||
event.unified_msg_origin = GLOBAL_UNIFIED_CONTEXT_UMO
|
||||
# Store original UMO for reference in later stages
|
||||
event.set_extra(ORIGINAL_UMO_KEY, original_umo)
|
||||
logger.debug(
|
||||
f"Global unified context mode enabled. Changed UMO from {original_umo} to {GLOBAL_UNIFIED_CONTEXT_UMO}"
|
||||
)
|
||||
|
||||
# apply unique session
|
||||
if self.unique_session and event.message_obj.type == MessageType.GROUP_MESSAGE:
|
||||
sid = build_unique_session_id(event)
|
||||
|
||||
@@ -21,3 +21,6 @@ class PlatformMetadata:
|
||||
"""平台是否支持真实流式传输"""
|
||||
support_proactive_message: bool = True
|
||||
"""平台是否支持主动消息推送(非用户触发)"""
|
||||
|
||||
module_path: str | None = None
|
||||
"""注册该适配器的模块路径,用于插件热重载时清理"""
|
||||
|
||||
@@ -37,6 +37,9 @@ def register_platform_adapter(
|
||||
if "id" not in default_config_tmpl:
|
||||
default_config_tmpl["id"] = adapter_name
|
||||
|
||||
# Get the module path of the class being decorated
|
||||
module_path = cls.__module__
|
||||
|
||||
pm = PlatformMetadata(
|
||||
name=adapter_name,
|
||||
description=desc,
|
||||
@@ -45,6 +48,7 @@ def register_platform_adapter(
|
||||
adapter_display_name=adapter_display_name,
|
||||
logo_path=logo_path,
|
||||
support_streaming_message=support_streaming_message,
|
||||
module_path=module_path,
|
||||
)
|
||||
platform_registry.append(pm)
|
||||
platform_cls_map[adapter_name] = cls
|
||||
@@ -52,3 +56,31 @@ def register_platform_adapter(
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def unregister_platform_adapters_by_module(module_path_prefix: str) -> list[str]:
|
||||
"""根据模块路径前缀注销平台适配器。
|
||||
|
||||
在插件热重载时调用,用于清理该插件注册的所有平台适配器。
|
||||
|
||||
Args:
|
||||
module_path_prefix: 模块路径前缀,如 "data.plugins.my_plugin"
|
||||
|
||||
Returns:
|
||||
被注销的平台适配器名称列表
|
||||
"""
|
||||
unregistered = []
|
||||
to_remove = []
|
||||
|
||||
for pm in platform_registry:
|
||||
if pm.module_path and pm.module_path.startswith(module_path_prefix):
|
||||
to_remove.append(pm)
|
||||
unregistered.append(pm.name)
|
||||
|
||||
for pm in to_remove:
|
||||
platform_registry.remove(pm)
|
||||
if pm.name in platform_cls_map:
|
||||
del platform_cls_map[pm.name]
|
||||
logger.debug(f"平台适配器 {pm.name} 已注销 (来自模块 {pm.module_path})")
|
||||
|
||||
return unregistered
|
||||
|
||||
@@ -15,6 +15,7 @@ import yaml
|
||||
from astrbot.core import logger, pip_installer, sp
|
||||
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.platform.register import unregister_platform_adapters_by_module
|
||||
from astrbot.core.provider.register import llm_tools
|
||||
from astrbot.core.utils.astrbot_path import (
|
||||
get_astrbot_config_path,
|
||||
@@ -842,6 +843,18 @@ class PluginManager:
|
||||
for func_tool in to_remove:
|
||||
llm_tools.func_list.remove(func_tool)
|
||||
|
||||
# Unregister platform adapters registered by this plugin
|
||||
# module_path is like "data.plugins.my_plugin.main", extract prefix like "data.plugins.my_plugin"
|
||||
module_prefix = ".".join(plugin_module_path.split(".")[:-1])
|
||||
if module_prefix:
|
||||
unregistered_adapters = unregister_platform_adapters_by_module(
|
||||
module_prefix
|
||||
)
|
||||
for adapter_name in unregistered_adapters:
|
||||
logger.info(
|
||||
f"移除了插件 {plugin_name} 的平台适配器 {adapter_name}",
|
||||
)
|
||||
|
||||
if plugin is None:
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
- 控制台页面新增调试提示和本地化文件 ([#4852](https://github.com/AstrBotDevs/AstrBot/pull/4852))
|
||||
|
||||
### 修复
|
||||
- 修复插件热重载时平台适配器未清理导致注册冲突的问题 ([#4859](https://github.com/AstrBotDevs/AstrBot/pull/4859))
|
||||
|
||||
### 其他
|
||||
- 更新 ruff 版本至 0.15.0
|
||||
- 新增 robots.txt ([#4847](https://github.com/AstrBotDevs/AstrBot/pull/4847))
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
- Add debug hint to console page and localization files ([#4852](https://github.com/AstrBotDevs/AstrBot/pull/4852))
|
||||
|
||||
### Bug Fixes
|
||||
- Fix platform adapter not being cleaned up during plugin hot reload, causing registration conflicts ([#4859](https://github.com/AstrBotDevs/AstrBot/pull/4859))
|
||||
|
||||
### Others
|
||||
- Update ruff version to 0.15.0
|
||||
- Add robots.txt ([#4847](https://github.com/AstrBotDevs/AstrBot/pull/4847))
|
||||
@@ -0,0 +1,4 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复
|
||||
- 修复 `on_llm_request` 钩子可能无法应用效果的问题
|
||||
@@ -0,0 +1,4 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复
|
||||
- 修复 token 统计错误的问题,修复在多轮 tool call 情况下或者其他极端情况下可能造成 tool 无限调用的问题。
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -11,5 +11,8 @@
|
||||
"mirrorLabel": "Force PyPI repository URL (optional)",
|
||||
"mirrorHint": "Force PyPI repository URL > Config item `PyPI Repository Address`",
|
||||
"installButton": "Install"
|
||||
},
|
||||
"debugHint": {
|
||||
"text": "Debug logs can be enabled in \"Configuration File → System → Console Log Level\""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,8 @@
|
||||
"mirrorLabel": "强制 PyPI 软件仓库链接(可选)",
|
||||
"mirrorHint": "强制 PyPI 软件仓库链接 > 配置项 `PyPI 软件仓库地址`",
|
||||
"installButton": "安装"
|
||||
},
|
||||
"debugHint": {
|
||||
"text": "Debug 日志需要在「配置文件 → 系统 → 控制台日志级别」中开启"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ const PurpleThemeDark: ThemeTypes = {
|
||||
name: 'PurpleThemeDark',
|
||||
dark: true,
|
||||
variables: {
|
||||
'border-color': '#3c96ca',
|
||||
'border-color': '#1677ff',
|
||||
'carousel-control-size': 10
|
||||
},
|
||||
colors: {
|
||||
primary: '#3c96ca',
|
||||
secondary: '#2288b7',
|
||||
primary: '#1677ff',
|
||||
secondary: '#722ed1',
|
||||
info: '#03c9d7',
|
||||
success: '#52c41a',
|
||||
accent: '#FFAB91',
|
||||
|
||||
@@ -10,7 +10,18 @@ const { tm } = useModuleI18n('features/console');
|
||||
<div style="height: 100%;">
|
||||
<div
|
||||
style="background-color: var(--v-theme-surface); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; flex-direction: row; align-items: center; justify-content: space-between;">
|
||||
<h4>{{ tm('title') }}</h4>
|
||||
<div>
|
||||
<h4>{{ tm('title') }}</h4>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-2"
|
||||
style="max-width: 600px;"
|
||||
>
|
||||
{{ tm('debugHint.text') }}
|
||||
</v-alert>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-switch
|
||||
v-model="autoScrollEnabled"
|
||||
@@ -111,4 +122,4 @@ export default {
|
||||
.fade-in {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.14.1"
|
||||
version = "4.14.4"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -69,14 +69,14 @@ dev = [
|
||||
"pytest>=8.4.1",
|
||||
"pytest-asyncio>=1.1.0",
|
||||
"pytest-cov>=6.2.1",
|
||||
"ruff>=0.12.8",
|
||||
"ruff>=0.15.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
astrbot = "astrbot.cli.__main__:cli"
|
||||
|
||||
[tool.ruff]
|
||||
exclude = ["astrbot/core/utils/t2i/local_strategy.py", "astrbot/api/all.py"]
|
||||
exclude = ["astrbot/core/utils/t2i/local_strategy.py", "astrbot/api/all.py", "tests"]
|
||||
line-length = 88
|
||||
target-version = "py310"
|
||||
|
||||
@@ -97,6 +97,7 @@ ignore = [
|
||||
"F405",
|
||||
"E501",
|
||||
"ASYNC230", # TODO: handle ASYNC230 in AstrBot
|
||||
"ASYNC240", # TODO: handle ASYNC240 in AstrBot
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
|
||||
Reference in New Issue
Block a user