Compare commits

...

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 31aae304c2 refactor: extract helper method to reduce code duplication
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2026-02-06 03:16:02 +00:00
copilot-swe-agent[bot] c6118409d0 feat: deactivate built-in web_search tools when disabled to allow MCP tools
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2026-02-06 03:12:51 +00:00
copilot-swe-agent[bot] f009e09894 Initial plan 2026-02-06 03:08:02 +00:00
Soulter fc2a67188f docs: update watashiwakoseinodesukara
Removed duplicate text and added a new image.
2026-02-05 23:08:14 +08:00
boushi1111 d69592aaa8 fix: TypeError when MCP schema type is a list (#4867)
* Fix TypeError when MCP schema type is a list

Fixes crash in Gemini native tools with VRChat MCP.

* Refactor: avoid modifying schema in place per feedback

* Fix formatting and cleanup comments
2026-02-05 22:51:29 +08:00
Dt8333 f3397f6f08 fix: pyright lint (#4874)
* feat: 将 MessageSession 的 platform_id 改为 init=False,实例化时无需传入

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* refactor: 将 isinstance 检查改为元组、将默认模型值设为空字符串、将类型注解改为 Any 并导入

* refactor: 为 _serialize_job 增加返回类型注解 dict

* fix: 使用 cast 获取百度 AIP 的 msg 并对 psutil_addr 引入 type: ignore

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* refactor: 引入 _AddrWithPort 协议并替换 conn.laddr 的 cast

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* fix: 在构建 AstrBotMessage 时对 ctx.channel 可能为 None 进行兜底处理

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

---------

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>
2026-02-05 21:54:12 +08:00
LIghtJUNction be92e4f395 feat: systemd support (#4880) 2026-02-05 21:52:21 +08:00
11 changed files with 82 additions and 15 deletions
+3 -2
View File
@@ -264,8 +264,9 @@ pre-commit install
<div align="center"> <div align="center">
_陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。_
_私は、高性能ですから!_ _私は、高性能ですから!_
陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。 <img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
@@ -49,6 +49,24 @@ class Main(star.Star):
self.sogo_search = Sogo() self.sogo_search = Sogo()
self.baidu_initialized = False self.baidu_initialized = False
# Deactivate built-in web search tools if web_search is disabled
# This allows MCP to provide custom web_search tools
websearch_enable = (provider_settings or {}).get("web_search", False)
if not websearch_enable:
self._set_tools_active(False)
def _set_tools_active(self, active: bool) -> None:
"""Set the active status of all built-in web search tools.
Args:
active: True to activate tools, False to deactivate them
"""
func_tool_mgr = self.context.get_llm_tool_manager()
for tool_name in self.TOOLS:
tool = func_tool_mgr.get_func(tool_name)
if tool:
tool.active = active
async def _tidy_text(self, text: str) -> str: async def _tidy_text(self, text: str) -> str:
"""清理文本,去除空格、换行符等""" """清理文本,去除空格、换行符等"""
return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ") return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
@@ -394,6 +412,10 @@ class Main(star.Star):
websearch_enable = prov_settings.get("web_search", False) websearch_enable = prov_settings.get("web_search", False)
provider = prov_settings.get("websearch_provider", "default") provider = prov_settings.get("websearch_provider", "default")
# Globally activate/deactivate built-in web search tools based on config
# This allows MCP to provide custom web_search tools when built-in is disabled
self._set_tools_active(websearch_enable)
tool_set = req.func_tool tool_set = req.func_tool
if isinstance(tool_set, FunctionToolManager): if isinstance(tool_set, FunctionToolManager):
req.func_tool = tool_set.get_full_tool_set() req.func_tool = tool_set.get_full_tool_set()
+12 -2
View File
@@ -246,8 +246,18 @@ class ToolSet:
result = {} result = {}
if "type" in schema and schema["type"] in supported_types: # Avoid side effects by not modifying the original schema
result["type"] = schema["type"] origin_type = schema.get("type")
target_type = origin_type
# Compatibility fix: Gemini API expects 'type' to be a string (enum),
# but standard JSON Schema (MCP) allows lists (e.g. ["string", "null"]).
# We fallback to the first non-null type.
if isinstance(origin_type, list):
target_type = next((t for t in origin_type if t != "null"), "string")
if target_type in supported_types:
result["type"] = target_type
if "format" in schema and schema["format"] in supported_formats.get( if "format" in schema and schema["format"] in supported_formats.get(
result["type"], result["type"],
set(), set(),
@@ -1,5 +1,7 @@
"""使用此功能应该先 pip install baidu-aip""" """使用此功能应该先 pip install baidu-aip"""
from typing import Any, cast
from aip import AipContentCensor from aip import AipContentCensor
from . import ContentSafetyStrategy from . import ContentSafetyStrategy
@@ -23,7 +25,8 @@ class BaiduAipStrategy(ContentSafetyStrategy):
count = len(res["data"]) count = len(res["data"])
parts = [f"百度审核服务发现 {count} 处违规:\n"] parts = [f"百度审核服务发现 {count} 处违规:\n"]
for i in res["data"]: for i in res["data"]:
parts.append(f"{i['msg']}\n") # 百度 AIP 返回结构是动态 dict;类型检查时 i 可能被推断为序列,转成 dict 后用 get 取字段
parts.append(f"{cast(dict[str, Any], i).get('msg', '')}\n")
parts.append("\n判断结果:" + res["conclusion"]) parts.append("\n判断结果:" + res["conclusion"])
info = "".join(parts) info = "".join(parts)
return False, info return False, info
+2 -2
View File
@@ -1,4 +1,4 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from astrbot.core.platform.message_type import MessageType from astrbot.core.platform.message_type import MessageType
@@ -13,7 +13,7 @@ class MessageSession:
"""平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。""" """平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。"""
message_type: MessageType message_type: MessageType
session_id: str session_id: str
platform_id: str | None = None platform_id: str = field(init=False)
def __str__(self): def __str__(self):
return f"{self.platform_id}:{self.message_type.value}:{self.session_id}" return f"{self.platform_id}:{self.message_type.value}:{self.session_id}"
@@ -444,9 +444,20 @@ class DiscordPlatformAdapter(Platform):
logger.warning(f"[Discord] 指令 '{cmd_name}' defer 失败: {e}") logger.warning(f"[Discord] 指令 '{cmd_name}' defer 失败: {e}")
# 2. 构建 AstrBotMessage # 2. 构建 AstrBotMessage
channel = ctx.channel
abm = AstrBotMessage() abm = AstrBotMessage()
abm.type = self._get_message_type(ctx.channel, ctx.guild_id) if channel is not None:
abm.group_id = self._get_channel_id(ctx.channel) abm.type = self._get_message_type(channel, ctx.guild_id)
abm.group_id = self._get_channel_id(channel)
else:
# 防守式兜底:channel 取不到时,仍能根据 guild_id/channel_id 推断会话信息
abm.type = (
MessageType.GROUP_MESSAGE
if ctx.guild_id is not None
else MessageType.FRIEND_MESSAGE
)
abm.group_id = str(ctx.channel_id)
abm.message_str = message_str_for_filter abm.message_str = message_str_for_filter
abm.sender = MessageMember( abm.sender = MessageMember(
user_id=str(ctx.author.id), user_id=str(ctx.author.id),
@@ -63,7 +63,7 @@ class ProviderFishAudioTTSAPI(TTSProvider):
self.headers = { self.headers = {
"Authorization": f"Bearer {self.chosen_api_key}", "Authorization": f"Bearer {self.chosen_api_key}",
} }
self.set_model(provider_config.get("model", None)) self.set_model(provider_config.get("model", ""))
async def _get_reference_id_by_character(self, character: str) -> str | None: async def _get_reference_id_by_character(self, character: str) -> str | None:
"""获取角色的reference_id """获取角色的reference_id
+1 -1
View File
@@ -23,7 +23,7 @@ class CronRoute(Route):
] ]
self.register_routes() self.register_routes()
def _serialize_job(self, job): def _serialize_job(self, job) -> dict:
data = job.model_dump() if hasattr(job, "model_dump") else job.__dict__ data = job.model_dump() if hasattr(job, "model_dump") else job.__dict__
for k in ["created_at", "updated_at", "last_run_at", "next_run_time"]: for k in ["created_at", "updated_at", "last_run_at", "next_run_time"]:
if isinstance(data.get(k), datetime): if isinstance(data.get(k), datetime):
+2 -1
View File
@@ -4,6 +4,7 @@ import asyncio
import os import os
import traceback import traceback
import uuid import uuid
from typing import Any
import aiofiles import aiofiles
from quart import request from quart import request
@@ -75,7 +76,7 @@ class KnowledgeBaseRoute(Route):
} }
def _set_task_result( def _set_task_result(
self, task_id: str, status: str, result: any = None, error: str | None = None self, task_id: str, status: str, result: Any = None, error: str | None = None
) -> None: ) -> None:
self.upload_tasks[task_id] = { self.upload_tasks[task_id] = {
"status": status, "status": status,
+7 -3
View File
@@ -2,14 +2,13 @@ import asyncio
import logging import logging
import os import os
import socket import socket
from typing import cast from typing import Protocol, cast
import jwt import jwt
import psutil import psutil
from flask.json.provider import DefaultJSONProvider from flask.json.provider import DefaultJSONProvider
from hypercorn.asyncio import serve from hypercorn.asyncio import serve
from hypercorn.config import Config as HyperConfig from hypercorn.config import Config as HyperConfig
from psutil._common import addr as psutil_addr
from quart import Quart, g, jsonify, request from quart import Quart, g, jsonify, request
from quart.logging import default_handler from quart.logging import default_handler
@@ -29,6 +28,11 @@ from .routes.session_management import SessionManagementRoute
from .routes.subagent import SubAgentRoute from .routes.subagent import SubAgentRoute
from .routes.t2i import T2iRoute from .routes.t2i import T2iRoute
class _AddrWithPort(Protocol):
port: int
APP: Quart APP: Quart
@@ -168,7 +172,7 @@ class AstrBotDashboard:
"""获取占用端口的进程详细信息""" """获取占用端口的进程详细信息"""
try: try:
for conn in psutil.net_connections(kind="inet"): for conn in psutil.net_connections(kind="inet"):
if cast(psutil_addr, conn.laddr).port == port: if cast(_AddrWithPort, conn.laddr).port == port:
try: try:
process = psutil.Process(conn.pid) process = psutil.Process(conn.pid)
# 获取详细信息 # 获取详细信息
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=AstrBot Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/.local/share/astrbot
ExecStart=/usr/bin/sh -c '/usr/bin/astrbot run || { /usr/bin/astrbot init && /usr/bin/astrbot run; }'
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=default.target