Compare commits

...

17 Commits

Author SHA1 Message Date
Soulter debc048659 chore: bump version to 4.1.3 2025-09-16 13:16:21 +08:00
邹永赫 92f5c918dd Merge pull request #2778 from MliKiowa/fix-handler-type
fix: parameter type/default handling in CommandFilter
2025-09-16 13:43:53 +09:00
手瓜一十雪 9519f1e8e2 fix: parameter type/default handling in CommandFilter
Adjusts logic to prioritize type annotations over default values when setting handler_params in CommandFilter. This ensures that parameter types are correctly inferred when available.
2025-09-16 11:49:27 +08:00
Soulter a8f874bf05 fix: 修复分段回复时,引用消息单独发送导致第一条消息内容为空的问题 (#2757) 2025-09-16 10:45:39 +08:00
anka 9d9917e45b feat: 增加群名称识别到 system prompt, 并提供相应的配置 (#2770)
* feat🤖: 增加群名称识别到system prompt, 并提供相应的配置

* feat: 优化实现方式, 重构AstrBotMessage, 向后兼容

* style: format
2025-09-16 10:23:08 +08:00
Soulter 91ee0a870d fix: handle image value correctly for mcp BlobResourceContents (#2753) 2025-09-16 08:22:18 +08:00
dependabot[bot] 6cbbffc5a9 chore(deps): bump the github-actions group with 2 updates (#2771)
Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `actions/checkout` from 4 to 5
- [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/v4...v5)

Updates `actions/setup-python` from 5 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-python
  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-09-16 08:19:31 +08:00
Yokami 8f26fd34d1 feat: add copy button for service providers (#2767) 2025-09-15 22:17:00 +08:00
Soulter fda655f6d7 fix: 修复配置默认 TTS 或者 STT 模型之后仍无法生效的问题 (#2758)
fixes: #2731
2025-09-15 22:08:40 +08:00
Soulter a663d6509b chore: bump version to 4.1.2 2025-09-14 21:07:36 +08:00
Soulter 9ec8839efa perf: 检查服务提供商可用性时跳过未启用的提供商 2025-09-14 21:01:32 +08:00
Soulter a7a0350eb2 fix: 平台配置下的「内容安全」组无法生效 (#2751) 2025-09-14 20:25:53 +08:00
Soulter 39a7a0d960 fix: revert "feat: 兼容指令名和第一个参数之间没有空格的情况 (#2650)" for command issue
This reverts commit 9bfa726107.
2025-09-14 19:31:15 +08:00
Soulter 7740e1e131 ci: add ci stage of code format checking (#2750)
* style: ruff format

* ci(dashboard-ci): ensure GitHub Release action only runs on push events

* ci(code-format): ruff format and ruff check
2025-09-14 18:05:58 +08:00
Soulter 9dce1ed47e chore(github): revise PR template
Updated the pull request template to improve clarity and fix formatting issues.
2025-09-14 14:44:46 +08:00
Soulter e84a00d3a5 fix: 修复多配置文件配置的不同人格无法生效的问题 (#2739)
fixes: #2724
2025-09-14 14:09:46 +08:00
anka 88a944cb57 chore(github): 优化 PR 模板 2025-09-14 12:58:34 +08:00
55 changed files with 584 additions and 270 deletions
+39 -12
View File
@@ -1,19 +1,46 @@
<!-- 如果有的话,指定这个 PR 解决的 ISSUE -->
解决了 #XYZ
<!-- 如果有的话,指定 PR 旨在解决的 ISSUE 编号。 -->
<!-- If applicable, please specify the ISSUE number this PR aims to resolve. -->
### Motivation
fixes #XYZ
<!--解释为什么要改动-->
---
### Modifications
### Motivation / 动机
<!--简单解释你的改动-->
<!--请描述此项更改的动机:它解决了什么问题?(例如:修复了 XX 错误,添加了 YY 功能)-->
<!--Please describe the motivation for this change: What problem does it solve? (e.g., Fixes XX bug, adds YY feature)-->
### Check
### Modifications / 改动点
<!--如果分支被合并,您的代码将服务于数万名用户!在提交前,请核查一下几点内容-->
<!--请总结你的改动:哪些核心文件被修改了?实现了什么功能?-->
<!--Please summarize your changes: What core files were modified? What functionality was implemented?-->
- [ ] 😊 我的 Commit Message 符合良好的[规范](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- [ ] 👀 我的更改经过良好的测试
- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt``pyproject.toml` 文件相应位置。
- [ ] 😮 我的更改没有引入恶意代码
### Verification Steps / 验证步骤
<!--请为审查者 (Reviewer) 提供清晰、可复现的验证步骤(例如:1. 导航到... 2. 点击...)。-->
<!--Please provide clear and reproducible verification steps for the Reviewer (e.g., 1. Navigate to... 2. Click...).-->
### Screenshots or Test Results / 运行截图或测试结果
<!--请粘贴截图、GIF 或测试日志,作为执行“验证步骤”的证据,证明此改动有效。-->
<!--Please paste screenshots, GIFs, or test logs here as evidence of executing the "Verification Steps" to prove this change is effective.-->
### Compatibility & Breaking Changes / 兼容性与破坏性变更
<!--请说明此变更的兼容性:哪些是破坏性变更?哪些地方做了向后兼容处理?是否提供了数据迁移方法?-->
<!--Please explain the compatibility of this change: What are the breaking changes? What backward-compatible measures were taken? Are data migration paths provided?-->
- [ ] 这是一个破坏性变更 (Breaking Change)。/ This is a breaking change.
- [ ] 这不是一个破坏性变更。/ This is NOT a breaking change.
---
### Checklist / 检查清单
<!--如果分支被合并,您的代码将服务于数万名用户!在提交前,请核查一下几点内容。-->
<!--If merged, your code will serve tens of thousands of users! Please double-check the following items before submitting.-->
- [ ] 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
- [ ] 👀 我的更改经过了良好的测试,**并已在上方提供了“验证步骤”和“运行截图”**。/ My changes have been well-tested, **and "Verification Steps" and "Screenshots" have been provided above**.
- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt``pyproject.toml` 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in `requirements.txt` and `pyproject.toml`.
- [ ] 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.
+34
View File
@@ -0,0 +1,34 @@
name: Code Format Check
on:
pull_request:
branches: [ master ]
push:
branches: [ master ]
jobs:
format-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.10'
- name: Install UV
run: pip install uv
- name: Install dependencies
run: uv sync
- name: Check code formatting with ruff
run: |
uv run ruff format --check .
- name: Check code style with ruff
run: |
uv run ruff check .
+1
View File
@@ -37,6 +37,7 @@ jobs:
!dist/**/*.md
- name: Create GitHub Release
if: github.event_name == 'push'
uses: ncipollo/release-action@v1
with:
tag: release-${{ github.sha }}
+22 -18
View File
@@ -124,15 +124,17 @@ def build_plug_list(plugins_dir: Path) -> list:
if metadata and all(
k in metadata for k in ["name", "desc", "version", "author", "repo"]
):
result.append({
"name": str(metadata.get("name", "")),
"desc": str(metadata.get("desc", "")),
"version": str(metadata.get("version", "")),
"author": str(metadata.get("author", "")),
"repo": str(metadata.get("repo", "")),
"status": PluginStatus.INSTALLED,
"local_path": str(plugin_dir),
})
result.append(
{
"name": str(metadata.get("name", "")),
"desc": str(metadata.get("desc", "")),
"version": str(metadata.get("version", "")),
"author": str(metadata.get("author", "")),
"repo": str(metadata.get("repo", "")),
"status": PluginStatus.INSTALLED,
"local_path": str(plugin_dir),
}
)
# 获取在线插件列表
online_plugins = []
@@ -142,15 +144,17 @@ def build_plug_list(plugins_dir: Path) -> list:
resp.raise_for_status()
data = resp.json()
for plugin_id, plugin_info in data.items():
online_plugins.append({
"name": str(plugin_id),
"desc": str(plugin_info.get("desc", "")),
"version": str(plugin_info.get("version", "")),
"author": str(plugin_info.get("author", "")),
"repo": str(plugin_info.get("repo", "")),
"status": PluginStatus.NOT_INSTALLED,
"local_path": None,
})
online_plugins.append(
{
"name": str(plugin_id),
"desc": str(plugin_info.get("desc", "")),
"version": str(plugin_info.get("version", "")),
"author": str(plugin_info.get("author", "")),
"repo": str(plugin_info.get("repo", "")),
"status": PluginStatus.NOT_INSTALLED,
"local_path": None,
}
)
except Exception as e:
click.echo(f"获取在线插件列表失败: {e}", err=True)
+1
View File
@@ -2,6 +2,7 @@ from dataclasses import dataclass
import typing as T
from astrbot.core.message.message_event_result import MessageChain
class AgentResponseData(T.TypedDict):
chain: MessageChain
+1
View File
@@ -14,4 +14,5 @@ class ContextWrapper(Generic[TContext]):
context: TContext
event: AstrMessageEvent
NoContext = ContextWrapper[None]
@@ -258,7 +258,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
)
yield MessageChain(
type="tool_direct_result"
).base64_image(res.content[0].data)
).base64_image(resource.blob)
else:
tool_call_result_blocks.append(
ToolCallMessageSegment(
+34 -11
View File
@@ -6,7 +6,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.1.1"
VERSION = "4.1.3"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
# 默认配置
@@ -60,6 +60,7 @@ DEFAULT_CONFIG = {
"web_search_link": False,
"display_reasoning_text": False,
"identifier": False,
"group_name_display": False,
"datetime_system_prompt": True,
"default_personality": "default",
"persona_pool": ["*"],
@@ -1724,6 +1725,9 @@ CONFIG_METADATA_2 = {
"identifier": {
"type": "bool",
},
"group_name_display": {
"type": "bool",
},
"datetime_system_prompt": {
"type": "bool",
},
@@ -1903,17 +1907,31 @@ CONFIG_METADATA_3 = {
"_special": "select_provider",
"hint": "留空代表不使用。可用于不支持视觉模态的聊天模型。",
},
"provider_stt_settings.enable": {
"description": "默认启用语音转文本",
"type": "bool",
},
"provider_stt_settings.provider_id": {
"description": "语音转文本模型",
"type": "string",
"hint": "留空代表不使用。",
"_special": "select_provider_stt",
"condition": {
"provider_stt_settings.enable": True,
},
},
"provider_tts_settings.enable": {
"description": "默认启用文本转语音",
"type": "bool",
},
"provider_tts_settings.provider_id": {
"description": "文本转语音模型",
"type": "string",
"hint": "留空代表不使用。",
"_special": "select_provider_tts",
"condition": {
"provider_tts_settings.enable": True,
},
},
"provider_settings.image_caption_prompt": {
"description": "图片转述提示词",
@@ -1983,6 +2001,11 @@ CONFIG_METADATA_3 = {
"description": "用户识别",
"type": "bool",
},
"provider_settings.group_name_display": {
"description": "显示群名称",
"type": "bool",
"hint": "启用后,在支持的平台(aiocqhttp)上会在 prompt 中包含群名称信息。",
},
"provider_settings.datetime_system_prompt": {
"description": "现实世界时间感知",
"type": "bool",
@@ -2130,41 +2153,41 @@ CONFIG_METADATA_3 = {
"description": "内容安全",
"type": "object",
"items": {
"platform_settings.content_safety.also_use_in_response": {
"content_safety.also_use_in_response": {
"description": "同时检查模型的响应内容",
"type": "bool",
},
"platform_settings.content_safety.baidu_aip.enable": {
"content_safety.baidu_aip.enable": {
"description": "使用百度内容安全审核",
"type": "bool",
"hint": "您需要手动安装 baidu-aip 库。",
},
"platform_settings.content_safety.baidu_aip.app_id": {
"content_safety.baidu_aip.app_id": {
"description": "App ID",
"type": "string",
"condition": {
"platform_settings.content_safety.baidu_aip.enable": True,
"content_safety.baidu_aip.enable": True,
},
},
"platform_settings.content_safety.baidu_aip.api_key": {
"content_safety.baidu_aip.api_key": {
"description": "API Key",
"type": "string",
"condition": {
"platform_settings.content_safety.baidu_aip.enable": True,
"content_safety.baidu_aip.enable": True,
},
},
"platform_settings.content_safety.baidu_aip.secret_key": {
"content_safety.baidu_aip.secret_key": {
"description": "Secret Key",
"type": "string",
"condition": {
"platform_settings.content_safety.baidu_aip.enable": True,
"content_safety.baidu_aip.enable": True,
},
},
"platform_settings.content_safety.internal_keywords.enable": {
"content_safety.internal_keywords.enable": {
"description": "关键词检查",
"type": "bool",
},
"platform_settings.content_safety.internal_keywords.extra_keywords": {
"content_safety.internal_keywords.extra_keywords": {
"description": "额外关键词",
"type": "list",
"items": {"type": "string"},
+1 -1
View File
@@ -53,7 +53,7 @@ async def do_migration_v4(
await migration_webchat_data(db_helper, platform_id_map)
# 执行偏好设置迁移
await migration_preferences(db_helper,platform_id_map)
await migration_preferences(db_helper, platform_id_map)
# 执行平台统计表迁移
await migration_platform_table(db_helper, platform_id_map)
@@ -5,6 +5,7 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
_VT = TypeVar("_VT")
class SharedPreferences:
def __init__(self, path=None):
if path is None:
@@ -42,4 +43,5 @@ class SharedPreferences:
self._data.clear()
self._save_preferences()
sp = SharedPreferences()
+2 -1
View File
@@ -4,6 +4,7 @@ from astrbot.core.db.po import Platform, Stats
from typing import Tuple, List, Dict, Any
from dataclasses import dataclass
@dataclass
class Conversation:
"""LLM 对话存储
@@ -76,7 +77,7 @@ PRAGMA encoding = 'UTF-8';
"""
class SQLiteDatabase():
class SQLiteDatabase:
def __init__(self, db_path: str) -> None:
super().__init__()
self.db_path = db_path
@@ -1,3 +1,3 @@
from .vec_db import FaissVecDB
__all__ = ["FaissVecDB"]
__all__ = ["FaissVecDB"]
+2 -1
View File
@@ -113,7 +113,8 @@ class FaissVecDB(BaseVecDB):
reranked_results, key=lambda x: x.relevance_score, reverse=True
)
top_k_results = [
top_k_results[reranked_result.index] for reranked_result in reranked_results
top_k_results[reranked_result.index]
for reranked_result in reranked_results
]
return top_k_results
+1 -1
View File
@@ -77,7 +77,7 @@ async def call_event_hook(
Returns:
bool: 如果事件被终止,返回 True
# """
#"""
handlers = star_handlers_registry.get_handlers_by_event_type(
hook_type, plugins_name=event.plugins_name
)
+103 -77
View File
@@ -1,17 +1,15 @@
import random
import asyncio
import math
import traceback
import astrbot.core.message.components as Comp
from typing import Union, AsyncGenerator
from ..stage import register_stage, Stage
from ..context import PipelineContext
from ..context import PipelineContext, call_event_hook
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.message.message_event_result import MessageChain, ResultContentType
from astrbot.core import logger
from astrbot.core.message.message_event_result import BaseMessageComponent
from astrbot.core.star.star_handler import star_handlers_registry, EventType
from astrbot.core.star.star import star_map
from astrbot.core.message.components import BaseMessageComponent, ComponentType
from astrbot.core.star.star_handler import EventType
from astrbot.core.utils.path_util import path_Mapping
from astrbot.core.utils.session_lock import session_lock_manager
@@ -114,6 +112,43 @@ class RespondStage(Stage):
# 如果所有组件都为空
return True
def is_seg_reply_required(self, event: AstrMessageEvent) -> bool:
"""检查是否需要分段回复"""
if not self.enable_seg:
return False
if self.only_llm_result and not event.get_result().is_llm_result():
return False
if event.get_platform_name() in [
"qq_official",
"weixin_official_account",
"dingtalk",
]:
return False
return True
def _extract_comp(
self,
raw_chain: list[BaseMessageComponent],
extract_types: set[ComponentType],
modify_raw_chain: bool = True,
):
extracted = []
if modify_raw_chain:
remaining = []
for comp in raw_chain:
if comp.type in extract_types:
extracted.append(comp)
else:
remaining.append(comp)
raw_chain[:] = remaining
else:
extracted = [comp for comp in raw_chain if comp.type in extract_types]
return extracted
async def process(
self, event: AstrMessageEvent
) -> Union[None, AsyncGenerator[None, None]]:
@@ -123,7 +158,14 @@ class RespondStage(Stage):
if result.result_content_type == ResultContentType.STREAMING_FINISH:
return
logger.info(
f"Prepare to send - {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}"
)
if result.result_content_type == ResultContentType.STREAMING_RESULT:
if result.async_stream is None:
logger.warning("async_stream 为空,跳过发送。")
return
# 流式结果直接交付平台适配器处理
use_fallback = self.config.get("provider_settings", {}).get(
"streaming_segmented", False
@@ -148,87 +190,71 @@ class RespondStage(Stage):
except Exception as e:
logger.warning(f"空内容检查异常: {e}")
record_comps = [c for c in result.chain if isinstance(c, Comp.Record)]
non_record_comps = [
c for c in result.chain if not isinstance(c, Comp.Record)
]
if (
self.enable_seg
and (
(self.only_llm_result and result.is_llm_result())
or not self.only_llm_result
# 发送消息链
# Record 需要强制单独发送
need_separately = {ComponentType.Record}
if self.is_seg_reply_required(event):
header_comps = self._extract_comp(
result.chain,
{ComponentType.Reply, ComponentType.At},
modify_raw_chain=True,
)
and event.get_platform_name()
not in ["qq_official", "weixin_official_account", "dingtalk"]
):
decorated_comps = []
if self.reply_with_mention:
for comp in result.chain:
if isinstance(comp, Comp.At):
decorated_comps.append(comp)
result.chain.remove(comp)
break
if self.reply_with_quote:
for comp in result.chain:
if isinstance(comp, Comp.Reply):
decorated_comps.append(comp)
result.chain.remove(comp)
break
# leverage lock to guarentee the order of message sending among different events
if not result.chain or len(result.chain) == 0:
# may fix #2670
logger.warning(
f"实际消息链为空, 跳过发送阶段。header_chain: {header_comps}, actual_chain: {result.chain}"
)
return
async with session_lock_manager.acquire_lock(event.unified_msg_origin):
for rcomp in record_comps:
i = await self._calc_comp_interval(rcomp)
await asyncio.sleep(i)
try:
await event.send(MessageChain([rcomp]))
except Exception as e:
logger.error(f"发送消息失败: {e} chain: {result.chain}")
break
# 分段回复
for comp in non_record_comps:
for comp in result.chain:
i = await self._calc_comp_interval(comp)
await asyncio.sleep(i)
try:
await event.send(MessageChain([*decorated_comps, comp]))
decorated_comps = [] # 清空已发送的装饰组件
if comp.type in need_separately:
await event.send(MessageChain([comp]))
else:
await event.send(MessageChain([*header_comps, comp]))
header_comps.clear()
except Exception as e:
logger.error(f"发送消息失败: {e} chain: {result.chain}")
break
logger.error(
f"发送消息链失败: chain = {MessageChain([comp])}, error = {e}",
exc_info=True,
)
else:
for rcomp in record_comps:
if all(
comp.type in {ComponentType.Reply, ComponentType.At}
for comp in result.chain
):
# may fix #2670
logger.warning(
f"消息链全为 Reply 和 At 消息段, 跳过发送阶段。chain: {result.chain}"
)
return
sep_comps = self._extract_comp(
result.chain,
need_separately,
modify_raw_chain=True,
)
for comp in sep_comps:
chain = MessageChain([comp])
try:
await event.send(MessageChain([rcomp]))
await event.send(chain)
except Exception as e:
logger.error(f"发送消息失败: {e} chain: {result.chain}")
logger.error(
f"发送消息链失败: chain = {chain}, error = {e}",
exc_info=True,
)
chain = MessageChain(result.chain)
if result.chain and len(result.chain) > 0:
try:
await event.send(chain)
except Exception as e:
logger.error(
f"发送消息链失败: chain = {chain}, error = {e}",
exc_info=True,
)
try:
await event.send(MessageChain(non_record_comps))
except Exception as e:
logger.error(traceback.format_exc())
logger.error(f"发送消息失败: {e} chain: {result.chain}")
logger.info(
f"AstrBot -> {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}"
)
handlers = star_handlers_registry.get_handlers_by_event_type(
EventType.OnAfterMessageSentEvent, plugins_name=event.plugins_name
)
for handler in handlers:
try:
logger.debug(
f"hook(on_after_message_sent) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
)
await handler.handler(event)
except BaseException:
logger.error(traceback.format_exc())
if event.is_stopped():
logger.info(
f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
)
return
if await call_event_hook(event, EventType.OnAfterMessageSentEvent):
return
event.clear_result()
+1 -1
View File
@@ -24,7 +24,7 @@ from astrbot.core.provider.entities import ProviderRequest
from astrbot.core.utils.metrics import Metric
from .astrbot_message import AstrBotMessage, Group
from .platform_metadata import PlatformMetadata
from .message_session import MessageSession, MessageSesion # noqa
from .message_session import MessageSession, MessageSesion # noqa
class AstrMessageEvent(abc.ABC):
+23 -1
View File
@@ -55,7 +55,7 @@ class AstrBotMessage:
self_id: str # 机器人的识别id
session_id: str # 会话id。取决于 unique_session 的设置。
message_id: str # 消息id
group_id: str = "" # 群组id,如果为私聊,则为空
group: Group # 群组
sender: MessageMember # 发送者
message: List[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式
message_str: str # 最直观的纯文本消息字符串
@@ -64,6 +64,28 @@ class AstrBotMessage:
def __init__(self) -> None:
self.timestamp = int(time.time())
self.group = None
def __str__(self) -> str:
return str(self.__dict__)
@property
def group_id(self) -> str:
"""
向后兼容的 group_id 属性
群组id,如果为私聊,则为空
"""
if self.group:
return self.group.group_id
return ""
@group_id.setter
def group_id(self, value: str):
"""设置 group_id"""
if value:
if self.group:
self.group.group_id = value
else:
self.group = Group(group_id=value)
else:
self.group = None
+9 -9
View File
@@ -68,23 +68,23 @@ class PlatformManager:
)
case "lark":
from .sources.lark.lark_adapter import (
LarkPlatformAdapter,
) # noqa: F401
LarkPlatformAdapter, # noqa: F401
)
case "dingtalk":
from .sources.dingtalk.dingtalk_adapter import (
DingtalkPlatformAdapter, # noqa: F401
)
case "telegram":
from .sources.telegram.tg_adapter import (
TelegramPlatformAdapter,
) # noqa: F401
TelegramPlatformAdapter, # noqa: F401
)
case "wecom":
from .sources.wecom.wecom_adapter import (
WecomPlatformAdapter,
) # noqa: F401
WecomPlatformAdapter, # noqa: F401
)
case "weixin_official_account":
from .sources.weixin_official_account.weixin_offacc_adapter import (
WeixinOfficialAccountPlatformAdapter, # noqa
WeixinOfficialAccountPlatformAdapter, # noqa: F401
)
case "discord":
from .sources.discord.discord_platform_adapter import (
@@ -94,8 +94,8 @@ class PlatformManager:
from .sources.slack.slack_adapter import SlackAdapter # noqa: F401
case "satori":
from .sources.satori.satori_adapter import (
SatoriPlatformAdapter,
) # noqa: F401
SatoriPlatformAdapter, # noqa: F401
)
except (ImportError, ModuleNotFoundError) as e:
logger.error(
f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。"
@@ -187,6 +187,7 @@ class AiocqhttpAdapter(Platform):
if event["message_type"] == "group":
abm.type = MessageType.GROUP_MESSAGE
abm.group_id = str(event.group_id)
abm.group.group_name = event.get("group_name", "N/A")
elif event["message_type"] == "private":
abm.type = MessageType.FRIEND_MESSAGE
if self.unique_session and abm.type == MessageType.GROUP_MESSAGE:
@@ -321,7 +322,9 @@ class AiocqhttpAdapter(Platform):
user_id=int(m["data"]["qq"]),
no_cache=False,
)
nickname = at_info.get("nick", "") or at_info.get("nickname", "")
nickname = at_info.get("nick", "") or at_info.get(
"nickname", ""
)
is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"}
abm.message.append(
@@ -54,9 +54,9 @@ class DingtalkMessageEvent(AstrMessageEvent):
logger.debug(f"send image: {ret}")
except Exception as e:
logger.error(f"钉钉图片处理失败: {e}")
logger.warning(f"跳过图片发送: {image_path}")
logger.warning(f"钉钉图片处理失败: {e}, 跳过图片发送")
continue
async def send(self, message: MessageChain):
await self.send_with_client(self.client, message)
await super().send(message)
@@ -41,7 +41,8 @@ class DiscordBotClient(discord.Bot):
await self.on_ready_once_callback()
except Exception as e:
logger.error(
f"[Discord] on_ready_once_callback 执行失败: {e}", exc_info=True)
f"[Discord] on_ready_once_callback 执行失败: {e}", exc_info=True
)
def _create_message_data(self, message: discord.Message) -> dict:
"""从 discord.Message 创建数据字典"""
@@ -90,7 +91,6 @@ class DiscordBotClient(discord.Bot):
message_data = self._create_message_data(message)
await self.on_message_received(message_data)
def _extract_interaction_content(self, interaction: discord.Interaction) -> str:
"""从交互中提取内容"""
interaction_type = interaction.type
@@ -79,9 +79,12 @@ class DiscordButton(BaseMessageComponent):
self.url = url
self.disabled = disabled
class DiscordReference(BaseMessageComponent):
"""Discord引用组件"""
type: str = "discord_reference"
def __init__(self, message_id: str, channel_id: str):
self.message_id = message_id
self.channel_id = channel_id
@@ -98,7 +101,6 @@ class DiscordView(BaseMessageComponent):
self.components = components or []
self.timeout = timeout
def to_discord_view(self) -> discord.ui.View:
"""转换为Discord View对象"""
view = discord.ui.View(timeout=self.timeout)
@@ -53,7 +53,13 @@ class DiscordPlatformEvent(AstrMessageEvent):
# 解析消息链为 Discord 所需的对象
try:
content, files, view, embeds, reference_message_id = await self._parse_to_discord(message)
(
content,
files,
view,
embeds,
reference_message_id,
) = await self._parse_to_discord(message)
except Exception as e:
logger.error(f"[Discord] 解析消息链时失败: {e}", exc_info=True)
return
@@ -206,8 +212,7 @@ class DiscordPlatformEvent(AstrMessageEvent):
if await asyncio.to_thread(path.exists):
file_bytes = await asyncio.to_thread(path.read_bytes)
files.append(
discord.File(BytesIO(file_bytes),
filename=i.name)
discord.File(BytesIO(file_bytes), filename=i.name)
)
else:
logger.warning(
@@ -94,10 +94,15 @@ class QQOfficialMessageEvent(AstrMessageEvent):
plain_text,
image_base64,
image_path,
record_file_path
record_file_path,
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
if not plain_text and not image_base64 and not image_path and not record_file_path:
if (
not plain_text
and not image_base64
and not image_path
and not record_file_path
):
return
payload = {
@@ -118,7 +123,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
)
payload["media"] = media
payload["msg_type"] = 7
if record_file_path: # group record msg
if record_file_path: # group record msg
media = await self.upload_group_and_c2c_record(
record_file_path, 3, group_openid=source.group_openid
)
@@ -134,9 +139,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
)
payload["media"] = media
payload["msg_type"] = 7
if record_file_path: # c2c record
if record_file_path: # c2c record
media = await self.upload_group_and_c2c_record(
record_file_path, 3, openid = source.author.user_openid
record_file_path, 3, openid=source.author.user_openid
)
payload["media"] = media
payload["msg_type"] = 7
@@ -190,58 +195,55 @@ class QQOfficialMessageEvent(AstrMessageEvent):
return await self.bot.api._http.request(route, json=payload)
async def upload_group_and_c2c_record(
self,
file_source: str,
file_type: int,
srv_send_msg: bool = False,
**kwargs
self, file_source: str, file_type: int, srv_send_msg: bool = False, **kwargs
) -> Optional[Media]:
"""
上传媒体文件
"""
# 构建基础payload
payload = {
"file_type": file_type,
"srv_send_msg": srv_send_msg
}
payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
# 处理文件数据
if os.path.exists(file_source):
# 读取本地文件
async with aiofiles.open(file_source, 'rb') as f:
async with aiofiles.open(file_source, "rb") as f:
file_content = await f.read()
# use base64 encode
payload["file_data"] = base64.b64encode(file_content).decode('utf-8')
payload["file_data"] = base64.b64encode(file_content).decode("utf-8")
else:
# 使用URL
payload["url"] = file_source
# 添加接收者信息和确定路由
if "openid" in kwargs:
payload["openid"] = kwargs["openid"]
route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
elif "group_openid" in kwargs:
payload["group_openid"] =kwargs["group_openid"]
route = Route("POST", "/v2/groups/{group_openid}/files", group_openid=kwargs["group_openid"])
payload["group_openid"] = kwargs["group_openid"]
route = Route(
"POST",
"/v2/groups/{group_openid}/files",
group_openid=kwargs["group_openid"],
)
else:
return None
try:
# 使用底层HTTP请求
result = await self.bot.api._http.request(route, json=payload)
if result:
return Media(
file_uuid=result.get("file_uuid"),
file_info=result.get("file_info"),
ttl=result.get("ttl", 0),
file_id=result.get("id", "")
file_id=result.get("id", ""),
)
except Exception as e:
logger.error(f"上传请求错误: {e}")
return None
async def post_c2c_message(
self,
openid: str,
@@ -286,19 +288,23 @@ class QQOfficialMessageEvent(AstrMessageEvent):
image_base64 = image_base64.removeprefix("base64://")
elif isinstance(i, Record):
if i.file:
record_wav_path = await i.convert_to_file_path() # wav 路径
record_wav_path = await i.convert_to_file_path() # wav 路径
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
record_tecent_silk_path = os.path.join(temp_dir, f"{uuid.uuid4()}.silk")
record_tecent_silk_path = os.path.join(
temp_dir, f"{uuid.uuid4()}.silk"
)
try:
duration = await wav_to_tencent_silk(record_wav_path, record_tecent_silk_path)
duration = await wav_to_tencent_silk(
record_wav_path, record_tecent_silk_path
)
if duration > 0:
record_file_path = record_tecent_silk_path
else:
record_file_path = None
record_file_path = None
logger.error("转换音频格式时出错:音频时长不大于0")
except Exception as e:
logger.error(f"处理语音时出错: {e}")
record_file_path = None
record_file_path = None
else:
logger.debug(f"qq_official 忽略 {i.type}")
return plain_text, image_base64, image_file_path, record_file_path
@@ -308,7 +308,9 @@ class SlackAdapter(Platform):
base64_content = base64.b64encode(content).decode("utf-8")
return base64_content
else:
logger.error(f"Failed to download slack file: {resp.status} {await resp.text()}")
logger.error(
f"Failed to download slack file: {resp.status} {await resp.text()}"
)
raise Exception(f"下载文件失败: {resp.status}")
async def run(self) -> Awaitable[Any]:
@@ -75,7 +75,13 @@ class SlackMessageEvent(AstrMessageEvent):
"text": {"type": "mrkdwn", "text": "文件上传失败"},
}
file_url = response["files"][0]["permalink"]
return {"type": "section", "text": {"type": "mrkdwn", "text": f"文件: <{file_url}|{segment.name or '文件'}>"}}
return {
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"文件: <{file_url}|{segment.name or '文件'}>",
},
}
else:
return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}
@@ -66,7 +66,9 @@ class TelegramPlatformEvent(AstrMessageEvent):
return chunks
@classmethod
async def send_with_client(cls, client: ExtBot, message: MessageChain, user_name: str):
async def send_with_client(
cls, client: ExtBot, message: MessageChain, user_name: str
):
image_path = None
has_reply = False
@@ -1,5 +1,6 @@
import asyncio
class WebChatQueueMgr:
def __init__(self) -> None:
self.queues = {}
@@ -30,4 +31,5 @@ class WebChatQueueMgr:
"""Check if a queue exists for the given conversation ID"""
return conversation_id in self.queues
webchat_queue_mgr = WebChatQueueMgr()
@@ -213,10 +213,10 @@ class WeChatPadProAdapter(Platform):
def _extract_auth_key(self, data):
"""Helper method to extract auth_key from response data."""
if isinstance(data, dict):
auth_keys = data.get("authKeys") # 新接口
auth_keys = data.get("authKeys") # 新接口
if isinstance(auth_keys, list) and auth_keys:
return auth_keys[0]
elif isinstance(data, list) and data: # 旧接口
elif isinstance(data, list) and data: # 旧接口
return data[0]
return None
@@ -234,7 +234,9 @@ class WeChatPadProAdapter(Platform):
try:
async with session.post(url, params=params, json=payload) as response:
if response.status != 200:
logger.error(f"生成授权码失败: {response.status}, {await response.text()}")
logger.error(
f"生成授权码失败: {response.status}, {await response.text()}"
)
return
response_data = await response.json()
@@ -245,7 +247,9 @@ class WeChatPadProAdapter(Platform):
if self.auth_key:
logger.info("成功获取授权码")
else:
logger.error(f"生成授权码成功但未找到授权码: {response_data}")
logger.error(
f"生成授权码成功但未找到授权码: {response_data}"
)
else:
logger.error(f"生成授权码失败: {response_data}")
except aiohttp.ClientConnectorError as e:
@@ -48,7 +48,12 @@ class WeChatKF(BaseWeChatAPI):
注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。
:return: 接口调用结果
"""
data = {"token": token, "cursor": cursor, "limit": limit, "open_kfid": open_kfid}
data = {
"token": token,
"cursor": cursor,
"limit": limit,
"open_kfid": open_kfid,
}
return self._post("kf/sync_msg", data=data)
def get_service_state(self, open_kfid, external_userid):
@@ -72,7 +77,9 @@ class WeChatKF(BaseWeChatAPI):
}
return self._post("kf/service_state/get", data=data)
def trans_service_state(self, open_kfid, external_userid, service_state, servicer_userid=""):
def trans_service_state(
self, open_kfid, external_userid, service_state, servicer_userid=""
):
"""
变更会话状态
@@ -180,7 +187,9 @@ class WeChatKF(BaseWeChatAPI):
"""
return self._get("kf/customer/get_upgrade_service_config")
def upgrade_service(self, open_kfid, external_userid, service_type, member=None, groupchat=None):
def upgrade_service(
self, open_kfid, external_userid, service_type, member=None, groupchat=None
):
"""
为客户升级为专员或客户群服务
@@ -246,7 +255,9 @@ class WeChatKF(BaseWeChatAPI):
data = {"open_kfid": open_kfid, "start_time": start_time, "end_time": end_time}
return self._post("kf/get_corp_statistic", data=data)
def get_servicer_statistic(self, start_time, end_time, open_kfid=None, servicer_userid=None):
def get_servicer_statistic(
self, start_time, end_time, open_kfid=None, servicer_userid=None
):
"""
获取「客户数据统计」接待人员明细数据
@@ -26,6 +26,7 @@ from optionaldict import optionaldict
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatKFMessage(BaseWeChatAPI):
"""
发送微信客服消息
@@ -125,35 +126,55 @@ class WeChatKFMessage(BaseWeChatAPI):
msg={"msgtype": "news", "link": {"link": articles_data}},
)
def send_msgmenu(self, user_id, open_kfid, head_content, menu_list, tail_content, msgid=""):
def send_msgmenu(
self, user_id, open_kfid, head_content, menu_list, tail_content, msgid=""
):
return self.send(
user_id,
open_kfid,
msgid,
msg={
"msgtype": "msgmenu",
"msgmenu": {"head_content": head_content, "list": menu_list, "tail_content": tail_content},
"msgmenu": {
"head_content": head_content,
"list": menu_list,
"tail_content": tail_content,
},
},
)
def send_location(self, user_id, open_kfid, name, address, latitude, longitude, msgid=""):
def send_location(
self, user_id, open_kfid, name, address, latitude, longitude, msgid=""
):
return self.send(
user_id,
open_kfid,
msgid,
msg={
"msgtype": "location",
"msgmenu": {"name": name, "address": address, "latitude": latitude, "longitude": longitude},
"msgmenu": {
"name": name,
"address": address,
"latitude": latitude,
"longitude": longitude,
},
},
)
def send_miniprogram(self, user_id, open_kfid, appid, title, thumb_media_id, pagepath, msgid=""):
def send_miniprogram(
self, user_id, open_kfid, appid, title, thumb_media_id, pagepath, msgid=""
):
return self.send(
user_id,
open_kfid,
msgid,
msg={
"msgtype": "miniprogram",
"msgmenu": {"appid": appid, "title": title, "thumb_media_id": thumb_media_id, "pagepath": pagepath},
"msgmenu": {
"appid": appid,
"title": title,
"thumb_media_id": thumb_media_id,
"pagepath": pagepath,
},
},
)
@@ -160,7 +160,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
self.wexin_event_workers[msg.id] = future
await self.convert_message(msg, future)
# I love shield so much!
result = await asyncio.wait_for(asyncio.shield(future), 60) # wait for 60s
result = await asyncio.wait_for(
asyncio.shield(future), 60
) # wait for 60s
logger.debug(f"Got future result: {result}")
self.wexin_event_workers.pop(msg.id, None)
return result # xml. see weixin_offacc_event.py
@@ -150,7 +150,6 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
return
logger.info(f"微信公众平台上传语音返回: {response}")
if active_send_mode:
self.client.message.send_voice(
message_obj.sender.user_id,
+1
View File
@@ -297,6 +297,7 @@ class LLMResponse:
)
return ret
@dataclass
class RerankResult:
index: int
+5 -1
View File
@@ -366,7 +366,10 @@ class ProviderManager:
if not self.curr_provider_inst:
self.curr_provider_inst = inst
elif provider_metadata.provider_type in [ProviderType.EMBEDDING, ProviderType.RERANK]:
elif provider_metadata.provider_type in [
ProviderType.EMBEDDING,
ProviderType.RERANK,
]:
inst = provider_metadata.cls_type(
provider_config, self.provider_settings
)
@@ -388,6 +391,7 @@ class ProviderManager:
# 和配置文件保持同步
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)
@@ -98,7 +98,7 @@ class ProviderFishAudioTTSAPI(TTSProvider):
# FishAudio的reference_id通常是32位十六进制字符串
# 例如: 626bb6d3f3364c9cbc3aa6a67300a664
pattern = r'^[a-fA-F0-9]{32}$'
pattern = r"^[a-fA-F0-9]{32}$"
return bool(re.match(pattern, reference_id.strip()))
async def _generate_request(self, text: str) -> dict:
+7 -12
View File
@@ -7,6 +7,7 @@ from astrbot.core.config import AstrBotConfig
from .custom_filter import CustomFilter
from ..star_handler import StarHandlerMetadata
class GreedyStr(str):
"""标记指令完成其他参数接收后的所有剩余文本。"""
@@ -51,10 +52,11 @@ class CommandFilter(HandlerFilter):
# 忽略前两个参数,即 self 和 event
idx += 1
continue
if v.default == inspect.Parameter.empty:
self.handler_params[k] = v.annotation
else:
# 优先类型注解 其次默认值
if v.annotation == inspect.Parameter.empty:
self.handler_params[k] = v.default
else:
self.handler_params[k] = v.annotation
def get_handler_md(self) -> StarHandlerMetadata:
return self.handler_md
@@ -152,17 +154,10 @@ class CommandFilter(HandlerFilter):
_full = f"{parent_command_name} {candidate}"
else:
_full = candidate
if message_str == _full:
# 完全等于命令名 → 没参数
message_str = ""
if message_str.startswith(f"{_full} ") or message_str == _full:
message_str = message_str[len(_full) :].strip()
ok = True
break
elif message_str.startswith(_full):
# 命令名后面无论是空格还是直接连参数都可以
message_str = message_str[len(_full):].lstrip()
ok = True
break
if not ok:
return False
+1 -2
View File
@@ -113,8 +113,7 @@ class CommandGroupFilter(HandlerFilter):
+ self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
)
raise ValueError(
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n"
+ tree
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" + tree
)
# complete_command_names = [name + " " for name in complete_command_names]
+4 -1
View File
@@ -84,7 +84,10 @@ class SessionPluginManager:
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
"session_plugin_config",
session_plugin_config,
scope="umo",
scope_id=session_id,
)
logger.info(
+2 -2
View File
@@ -791,11 +791,11 @@ class PluginManager:
if star_metadata.star_cls is None:
return
if '__del__' in star_metadata.star_cls_type.__dict__:
if "__del__" in star_metadata.star_cls_type.__dict__:
asyncio.get_event_loop().run_in_executor(
None, star_metadata.star_cls.__del__
)
elif 'terminate' in star_metadata.star_cls_type.__dict__:
elif "terminate" in star_metadata.star_cls_type.__dict__:
await star_metadata.star_cls.terminate()
async def turn_on_plugin(self, plugin_name: str):
+22 -8
View File
@@ -30,8 +30,13 @@ from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.core.star.context import Context
from astrbot.core.star.star import star_map
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import AiocqhttpMessageEvent
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_platform_adapter import AiocqhttpAdapter
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import (
AiocqhttpMessageEvent,
)
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_platform_adapter import (
AiocqhttpAdapter,
)
class StarTools:
"""
@@ -77,7 +82,11 @@ class StarTools:
@classmethod
async def send_message_by_id(
cls, type: str, id: str, message_chain: MessageChain, platform: str = "aiocqhttp"
cls,
type: str,
id: str,
message_chain: MessageChain,
platform: str = "aiocqhttp",
):
"""
根据 id(例如qq号, 群号等) 直接, 主动地发送消息
@@ -92,7 +101,9 @@ class StarTools:
raise ValueError("StarTools not initialized")
platforms = cls._context.platform_manager.get_insts()
if platform == "aiocqhttp":
adapter = next((p for p in platforms if isinstance(p, AiocqhttpAdapter)), None)
adapter = next(
(p for p in platforms if isinstance(p, AiocqhttpAdapter)), None
)
if adapter is None:
raise ValueError("未找到适配器: AiocqhttpAdapter")
await AiocqhttpMessageEvent.send_message(
@@ -115,7 +126,7 @@ class StarTools:
message_str: str,
message_id: str = "",
raw_message: object = None,
group_id: str = ""
group_id: str = "",
) -> AstrBotMessage:
"""
创建一个AstrBot消息对象
@@ -152,7 +163,6 @@ class StarTools:
@classmethod
async def create_event(
cls, abm: AstrBotMessage, platform: str = "aiocqhttp", is_wake: bool = True
) -> None:
"""
创建并提交事件到指定平台
@@ -167,7 +177,9 @@ class StarTools:
raise ValueError("StarTools not initialized")
platforms = cls._context.platform_manager.get_insts()
if platform == "aiocqhttp":
adapter = next((p for p in platforms if isinstance(p, AiocqhttpAdapter)), None)
adapter = next(
(p for p in platforms if isinstance(p, AiocqhttpAdapter)), None
)
if adapter is None:
raise ValueError("未找到适配器: AiocqhttpAdapter")
event = AiocqhttpMessageEvent(
@@ -277,7 +289,9 @@ class StarTools:
if not plugin_name:
raise ValueError("无法获取插件名称")
data_dir = Path(os.path.join(get_astrbot_data_path(), "plugin_data", plugin_name))
data_dir = Path(
os.path.join(get_astrbot_data_path(), "plugin_data", plugin_name)
)
try:
data_dir.mkdir(parents=True, exist_ok=True)
-1
View File
@@ -1,7 +1,6 @@
import typing
import traceback
import os
import copy
from .route import Route, Response, RouteContext
from astrbot.core.provider.entities import ProviderType
from quart import request
+12 -4
View File
@@ -10,7 +10,9 @@ class LogRoute(Route):
super().__init__(context)
self.log_broker = log_broker
self.app.add_url_rule("/api/live-log", view_func=self.log, methods=["GET"])
self.app.add_url_rule("/api/log-history", view_func=self.log_history, methods=["GET"])
self.app.add_url_rule(
"/api/log-history", view_func=self.log_history, methods=["GET"]
)
async def log(self):
async def stream():
@@ -48,9 +50,15 @@ class LogRoute(Route):
"""获取日志历史"""
try:
logs = list(self.log_broker.log_cache)
return Response().ok(data={
"logs": logs,
}).__dict__
return (
Response()
.ok(
data={
"logs": logs,
}
)
.__dict__
)
except BaseException as e:
logger.error(f"获取日志历史失败: {e}")
return Response().error(f"获取日志历史失败: {e}").__dict__
-1
View File
@@ -1,4 +1,3 @@
from astrbot.core import logger
from astrbot.core.config.astrbot_config import AstrBotConfig
from dataclasses import dataclass
from quart import Quart
-1
View File
@@ -1,6 +1,5 @@
import traceback
import aiohttp
from quart import request
from astrbot.core import logger
-12
View File
@@ -3,15 +3,3 @@
修复了 v4.1.0 `model referenced before assignment` 的错误。
> 如果已经使用自定义文转图模板,此次升级之后将会被覆盖,请提前备份。路径在 `astrbot/core/utils/t2i/template` 目录下。
0. ‼️‼️‼️ 修复 LLM 仍会调用已禁用的工具的问题 ([#2729](https://github.com/Soulter/AstrBot/issues/2729))
1. ‼️ 修复 WebChat 下,Agent 长时任务时,SSE 连接自动断开的问题
2. ‼️ 修复自定义文转图模板更新版本后会被覆盖的问题 ([#2677](https://github.com/Soulter/AstrBot/issues/2677))
3. 修复 Satori 适配器教程链接 ([#2668](https://github.com/Soulter/AstrBot/issues/2668))
4. 修复插件页表格视图中,点击状态字段表头排序不起作用的问题 ([#2714](https://github.com/Soulter/AstrBot/issues/2714))
5. 修复工具调用时的 content 内容在重新加载后没有显示在 webchat 的问题 ([#2727](https://github.com/Soulter/AstrBot/issues/2727))
6. 允许添加多个 tavily API Key 进行轮询 ([#2725](https://github.com/Soulter/AstrBot/issues/2725))
7. 添加 --webui-dir 启动参数以支持指定 WebUI 构建文件目录 ([#2680](https://github.com/Soulter/AstrBot/issues/2680))
8. 兼容指令名和第一个参数之间没有空格的情况 ([#2650](https://github.com/Soulter/AstrBot/issues/2650))
9. 支持在 WebUI 自定义 OpenAI API extra_body 参数 ([#2719](https://github.com/Soulter/AstrBot/issues/2719))
10. 增加 on_platform_loaded 钩子以在消息平台适配器实例化完成后触发 ([#2651](https://github.com/Soulter/AstrBot/issues/2651))
+9
View File
@@ -0,0 +1,9 @@
# What's Changed
0. ‼️‼️‼️ fix: 修复 4.1.1 版本下,指令调用异常的问题
1. ‼️‼️ fix: 修复多配置文件配置的不同人格无法生效的问题 ([#2739](https://github.com/AstrBotDevs/AstrBot/issues/2739))
2. ‼️‼️ fix: 修复人格所选择的工具无法应用的问题 ([#2739](https://github.com/AstrBotDevs/AstrBot/issues/2739))
3. ‼️‼️ fix: 修复平台配置下的「内容安全」组无法生效 ([#2751](https://github.com/AstrBotDevs/AstrBot/issues/2751))
4. perf: 检查服务提供商可用性时跳过未启用的提供商,解决部分 `provider with id xxx not found` 的问题
fixes: [#2724](https://github.com/AstrBotDevs/AstrBot/issues/2724)
+8
View File
@@ -0,0 +1,8 @@
# What's Changed
0. ‼️ fix: 修复 4.0.0 版本之后,配置默认 TTS 或者 STT 模型之后仍无法生效的问题 ([#2758](https://github.com/Soulter/AstrBot/issues/2758))
1. ‼️ fix: 修复分段回复时,引用消息单独发送导致第一条消息内容为空的问题 ([#2757](https://github.com/Soulter/AstrBot/issues/2757))
2. feat: 支持在 WebUI 复制提供商配置以简化操作 ([#2767](https://github.com/Soulter/AstrBot/issues/2767))
3. fix: handle image value correctly for mcp BlobResourceContents ([#2753](https://github.com/Soulter/AstrBot/issues/2753))
4. feat: 增加 QQ 群名称识别到 system prompt, 并提供相应的配置 ([#2770](https://github.com/Soulter/AstrBot/issues/2770))
5. fix: parameter type/default handling in CommandFilter
+22 -9
View File
@@ -4,28 +4,28 @@
<span class="text-h2 text-truncate" :title="getItemTitle()">{{ getItemTitle() }}</span>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-switch
color="primary"
hide-details
density="compact"
<v-switch
color="primary"
hide-details
density="compact"
:model-value="getItemEnabled()"
:loading="loading"
:disabled="loading"
v-bind="props"
v-bind="props"
@update:model-value="toggleEnabled"
></v-switch>
</template>
<span>{{ getItemEnabled() ? t('core.common.itemCard.enabled') : t('core.common.itemCard.disabled') }}</span>
</v-tooltip>
</v-card-title>
<v-card-text>
<slot name="item-details" :item="item"></slot>
</v-card-text>
<v-card-actions style="margin: 8px;">
<v-btn
variant="outlined"
variant="outlined"
color="error"
rounded="xl"
@click="$emit('delete', item)"
@@ -40,6 +40,15 @@
>
{{ t('core.common.itemCard.edit') }}
</v-btn>
<v-btn
v-if="showCopyButton"
variant="tonal"
color="secondary"
rounded="xl"
@click="$emit('copy', item)"
>
{{ t('core.common.itemCard.copy') }}
</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
@@ -83,9 +92,13 @@ export default {
loading: {
type: Boolean,
default: false
},
showCopyButton: {
type: Boolean,
default: false
}
},
emits: ['toggle-enabled', 'delete', 'edit'],
emits: ['toggle-enabled', 'delete', 'edit', 'copy'],
methods: {
getItemTitle() {
return this.item[this.titleField];
@@ -73,6 +73,7 @@
"disabled": "已禁用",
"delete": "删除",
"edit": "编辑",
"copy": "复制",
"noData": "暂无数据"
}
}
}
+67 -13
View File
@@ -56,14 +56,16 @@
<v-row v-else>
<v-col v-for="(provider, index) in filteredProviders" :key="index" cols="12" md="6" lg="4" xl="3">
<item-card
:item="provider"
title-field="id"
<item-card
:item="provider"
title-field="id"
enabled-field="enable"
@toggle-enabled="providerStatusChange"
:bglogo="getProviderIcon(provider.provider)"
@delete="deleteProvider"
@edit="configExistingProvider">
@delete="deleteProvider"
@edit="configExistingProvider"
@copy="copyProvider"
:show-copy-button="true">
<template v-slot:details="{ item }">
</template>
</item-card>
@@ -95,7 +97,7 @@
<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">
@@ -113,7 +115,7 @@
></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>
@@ -348,10 +350,10 @@ export default {
save_message_success: "success",
showConsole: false,
// 显示状态部分
showStatus: false,
// 供应商状态相关
providerStatuses: [],
loadingStatus: false,
@@ -437,7 +439,7 @@ export default {
}
};
},
// 根据选择的标签过滤提供商列表
filteredProviders() {
if (!this.config_data.provider || this.activeProviderTypeTab === 'all') {
@@ -449,7 +451,7 @@ export default {
if (provider.provider_type) {
return provider.provider_type === this.activeProviderTypeTab;
}
// 否则使用映射关系
const mappedType = this.oldVersionProviderTypeMapping[provider.type];
return mappedType === this.activeProviderTypeTab;
@@ -657,6 +659,40 @@ export default {
}
},
async copyProvider(providerToCopy) {
console.log('copyProvider triggered for:', providerToCopy);
// 1. 创建深拷贝
const newProviderConfig = JSON.parse(JSON.stringify(providerToCopy));
// 2. 生成唯一的 ID
const generateUniqueId = (baseId) => {
let newId = `${baseId}_copy`;
let counter = 1;
const existingIds = this.config_data.provider.map(p => p.id);
while (existingIds.includes(newId)) {
newId = `${baseId}_copy_${counter}`;
counter++;
}
return newId;
};
newProviderConfig.id = generateUniqueId(providerToCopy.id);
// 3. 设置为禁用状态,等待用户手动开启
newProviderConfig.enable = false;
this.loading = true;
try {
// 4. 调用后端接口创建
const res = await axios.post('/api/config/provider/new', newProviderConfig);
this.showSuccess(res.data.message || `成功复制并创建了 ${newProviderConfig.id}`);
this.getConfig(); // 5. 刷新列表
} catch (err) {
this.showError(err.response?.data?.message || err.message);
} finally {
this.loading = false;
}
},
deleteProvider(provider) {
if (confirm(this.tm('messages.confirm.delete', { id: provider.id }))) {
axios.post('/api/config/provider/delete', { id: provider.id }).then((res) => {
@@ -694,14 +730,14 @@ export default {
this.save_message_success = "error";
this.save_message_snack = true;
},
// 获取供应商状态
async fetchProviderStatus() {
if (this.loadingStatus) return;
this.loadingStatus = true;
this.showStatus = true; // 自动展开状态部分
// 1. 立即初始化UI为pending状态
this.providerStatuses = this.config_data.provider.map(p => ({
id: p.id,
@@ -712,6 +748,19 @@ export default {
// 2. 为每个provider创建一个并发的测试请求
const promises = this.config_data.provider.map(p => {
if (!p.enable) {
const index = this.providerStatuses.findIndex(s => s.id === p.id);
if (index !== -1) {
const disabledStatus = {
...this.providerStatuses[index],
status: 'unavailable',
error: '该提供商未被用户启用'
};
this.providerStatuses.splice(index, 1, disabledStatus);
}
return Promise.resolve();
}
return axios.get(`/api/config/provider/check_one?id=${p.id}`)
.then(res => {
if (res.data && res.data.status === 'ok') {
@@ -887,4 +936,9 @@ export default {
.v-window {
border-radius: 4px;
}
.status-card {
height: 120px;
overflow-y: auto;
}
</style>
+3 -1
View File
@@ -35,7 +35,9 @@ class LongTermMemory:
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_settings"][
"default_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"]
+12 -4
View File
@@ -1214,6 +1214,12 @@ UID: {user_id} 此 ID 可用于设置管理员。
user_info = f"\n[User ID: {user_id}, Nickname: {user_nickname}]\n"
req.prompt = user_info + req.prompt
if cfg.get("group_name_display") and event.message_obj.group_id:
group_name = event.message_obj.group.group_name
if group_name:
req.system_prompt += f"\nGroup name: {group_name}\n"
# 启用附加时间戳
if cfg.get("datetime_system_prompt"):
current_time = None
@@ -1232,11 +1238,13 @@ UID: {user_id} 此 ID 可用于设置管理员。
if req.conversation:
# persona inject
persona_id = req.conversation.persona_id
persona_id = req.conversation.persona_id or cfg.get("default_personality")
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
persona_id = self.context.persona_manager.selected_default_persona_v3[
"name"
]
default_persona = (
self.context.persona_manager.selected_default_persona_v3
)
if default_persona:
persona_id = default_persona["name"]
persona = next(
builtins.filter(
lambda persona: persona["name"] == persona_id,
+4 -2
View File
@@ -5,7 +5,7 @@ import astrbot.api.star as star
import astrbot.api.event.filter as filter
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.api.provider import ProviderRequest
from astrbot.api import llm_tool, agent, logger, AstrBotConfig
from astrbot.api import llm_tool, logger, AstrBotConfig
from astrbot.core.provider.func_tool_manager import FunctionToolManager
from .engines import SearchResult
from .engines.bing import Bing
@@ -35,7 +35,9 @@ class Main(star.Star):
if provider_settings:
tavily_key = provider_settings.get("websearch_tavily_key")
if isinstance(tavily_key, str):
logger.info("检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。")
logger.info(
"检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。"
)
if tavily_key:
provider_settings["websearch_tavily_key"] = [tavily_key]
else:
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.1.1"
version = "4.1.3"
description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md"
requires-python = ">=3.10"