Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
616e9ac
fix: 修复了国内配置一些模型不可用问题
Apr 20, 2026
f26aeb2
Merge remote-tracking branch 'upstream/master'
Apr 20, 2026
260fd26
Merge remote-tracking branch 'upstream/master'
Apr 20, 2026
7fa4421
feat: 提高代码复用性
Apr 20, 2026
269ec30
fix(network): reuse shared SSL context
zouyonghe Apr 21, 2026
c2443a8
test(network): cover proxy and header forwarding
zouyonghe Apr 21, 2026
e9a4eed
fix(network): support verify overrides
zouyonghe Apr 21, 2026
7df7fa8
Merge remote-tracking branch 'upstream/master'
Apr 21, 2026
836c489
Merge remote-tracking branch 'origin/master'
Apr 21, 2026
6ece296
fix: 修复了MCP工具对http/sse的兼容性问题
Apr 21, 2026
fef9822
fix: 打包后desktop不可用问题
Apr 21, 2026
5cc1b5d
Merge remote-tracking branch 'upstream/master'
Apr 22, 2026
a34c850
fix: 嵌入模型调用错误
Apr 22, 2026
4d571d5
fix: 嵌入模型调用错误
Apr 22, 2026
72e76da
fix: 非标准MCP json inputSchema 支持
Apr 22, 2026
1b3c979
fix: 从ModelScope平台同步mcp时自动探测类型,防止硬编码sse导致mcp服务不可用
Apr 22, 2026
fb23cb6
fix: uvx安装依赖存在的证书问题导致MCP安装异常
Apr 22, 2026
d1898e7
Merge remote-tracking branch 'upstream/master'
Apr 23, 2026
8757494
fix: 首轮对话AI不识别附件
Apr 23, 2026
45025eb
fix: 定时任务报错
Apr 23, 2026
08cac6b
Merge remote-tracking branch 'upstream/master'
Apr 23, 2026
a96b5c6
fix: 修复qq接入后长期运行一段时间后可能会出现用户发一条消息,后台重复处理回复给用户多条消息的问题
Apr 23, 2026
1ad600e
Merge remote-tracking branch 'upstream/master'
Apr 24, 2026
f14095f
feat: 优化国内docker部署体验,增加相关说明
Apr 24, 2026
72ee01a
Merge remote-tracking branch 'upstream/master'
Apr 25, 2026
43af9d4
fix: 修复deepseek使用错误
Apr 25, 2026
50eb424
fix: 修复deepseek使用错误
Apr 25, 2026
de6514b
Merge remote-tracking branch 'upstream/master'
Apr 29, 2026
e703bc7
fix: 嵌入模型英伟达兼容性
Apr 29, 2026
4a40bdc
Merge remote-tracking branch 'upstream/master'
May 2, 2026
029982e
fix: 修复dify流程不支持文件的问题
May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Dockerfile.cn
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
FROM python:3.12-slim
WORKDIR /AstrBot

# 国内镜像源加速
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources

COPY . /AstrBot/

RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
build-essential \
python3-dev \
libffi-dev \
libssl-dev \
ca-certificates \
bash \
ffmpeg \
libavcodec-extra \
curl \
gnupg \
git \
ripgrep \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN python -m pip install uv -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
&& echo "3.12" > .python-version \
&& uv lock \
&& uv export --format requirements.txt --output-file requirements.txt --frozen \
&& uv pip install -r requirements.txt --no-cache-dir --system --index-url https://mirrors.aliyun.com/pypi/simple/ \
&& uv pip install socksio uv pilk --no-cache-dir --system --index-url https://mirrors.aliyun.com/pypi/simple/

EXPOSE 6185

CMD ["python", "main.py"]
11 changes: 11 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ uv tool upgrade astrbot --python 3.12

请参考官方文档 [使用 Docker 部署 AstrBot](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot)。

#### 国内用户 Docker 加速构建

项目提供了国内镜像源加速的 `Dockerfile.cn` 和 `docker-compose.yml`,使用阿里云镜像源加速 apt 和 pip 依赖下载:

```bash
# 克隆项目后,使用国内加速配置构建并启动
docker compose -f docker-compose.yml up -d --build
```

构建完成后,通过 `http://<服务器IP>:6185` 访问 WebUI 进行初始化配置。数据持久化目录为 `./data`。

### 在 雨云 上部署

对于希望一键部署 AstrBot 且不想自行管理服务器的用户,我们推荐使用雨云的一键云部署服务 ☁️:
Expand Down
133 changes: 117 additions & 16 deletions astrbot/core/agent/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@
"Warning: Missing 'mcp' dependency or MCP library version too old, Streamable HTTP connection unavailable.",
)

try:
import httpx as _httpx

def _create_no_verify_httpx_client(
headers: dict[str, str] | None = None,
timeout: _httpx.Timeout | None = None,
auth: _httpx.Auth | None = None,
) -> _httpx.AsyncClient:
kwargs: dict[str, Any] = {
"follow_redirects": True,
"verify": False,
}
if timeout is None:
kwargs["timeout"] = _httpx.Timeout(30, read=300)
else:
kwargs["timeout"] = timeout
if headers is not None:
kwargs["headers"] = headers
if auth is not None:
kwargs["auth"] = auth
return _httpx.AsyncClient(**kwargs)

except (ModuleNotFoundError, ImportError):
_create_no_verify_httpx_client = None


def _prepare_config(config: dict) -> dict:
"""Prepare configuration, handle nested format"""
Expand Down Expand Up @@ -237,13 +262,57 @@ def validate_mcp_stdio_config(config: dict) -> None:
raise ValueError("MCP stdio env keys and values must be strings.")


def _get_certifi_ca_bundle() -> str | None:
"""Try to locate the certifi CA bundle for SSL_CERT_FILE."""
try:
import certifi

return certifi.where()
except ImportError:
pass
# Fallback: look for certifi in common locations
for candidate in (
os.path.join(
os.path.dirname(sys.executable),
"Lib",
"site-packages",
"certifi",
"cacert.pem",
),
os.path.join(
os.path.dirname(sys.executable),
"..",
"Lib",
"site-packages",
"certifi",
"cacert.pem",
),
):
if os.path.isfile(candidate):
return candidate
return None


def _prepare_stdio_env(config: dict) -> dict:
"""Preserve Windows executable resolution for stdio subprocesses."""
if sys.platform != "win32":
return config
"""Prepare environment variables for stdio subprocesses.

On Windows:
- Merges system environment variables (case-insensitive handling).
- For uv/uvx commands, sets SSL_CERT_FILE from certifi to avoid
``invalid peer certificate: UnknownIssuer`` errors caused by
uv's bundled TLS not trusting the system certificate store.
"""
prepared = config.copy()
env = dict(prepared.get("env") or {})
env = _merge_environment_variables(env)

if sys.platform == "win32":
command_name = _normalize_stdio_command_name(config.get("command", ""))
if command_name in ("uv", "uvx") and "SSL_CERT_FILE" not in env:
ca_bundle = _get_certifi_ca_bundle()
if ca_bundle:
env["SSL_CERT_FILE"] = ca_bundle

prepared["env"] = env
return prepared

Expand Down Expand Up @@ -326,6 +395,18 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
return False, f"{e!s}"


_NONSTANDARD_TYPE_MAP: dict[str, str] = {
"int": "integer",
"float": "number",
"double": "number",
"decimal": "number",
"bool": "boolean",
"str": "string",
"dict": "object",
"list": "array",
}


def _normalize_mcp_input_schema(schema: dict[str, Any]) -> dict[str, Any]:
"""Normalize common non-standard MCP JSON Schema variants.

Expand All @@ -334,6 +415,9 @@ def _normalize_mcp_input_schema(schema: dict[str, Any]) -> dict[str, Any]:
parent object to declare `required` as an array of property names instead.
We lift those booleans to the parent object so the schema remains usable
without disabling validation entirely.

Also normalizes non-standard type names (e.g. ``"int"`` → ``"integer"``,
``"str"`` → ``"string"``) that some MCP servers emit.
"""

def _normalize(node: Any) -> Any:
Expand All @@ -345,6 +429,16 @@ def _normalize(node: Any) -> Any:

normalized = {key: _normalize(value) for key, value in node.items()}

# Normalize non-standard type names
type_val = normalized.get("type")
if isinstance(type_val, str) and type_val in _NONSTANDARD_TYPE_MAP:
normalized["type"] = _NONSTANDARD_TYPE_MAP[type_val]
elif isinstance(type_val, list):
normalized["type"] = [
_NONSTANDARD_TYPE_MAP.get(t, t) if isinstance(t, str) else t
for t in type_val
]

properties = normalized.get("properties")
if isinstance(properties, dict):
original_properties = (
Expand Down Expand Up @@ -439,14 +533,22 @@ def logging_callback(
else:
raise Exception("MCP connection config missing transport or type field")

_http_client_kwargs: dict[str, Any] = {
"url": cfg["url"],
"headers": cfg.get("headers", {}),
}
if _create_no_verify_httpx_client is not None:
_http_client_kwargs["httpx_client_factory"] = (
_create_no_verify_httpx_client
)

if transport_type != "streamable_http":
# SSE transport method
self._streams_context = sse_client(
url=cfg["url"],
headers=cfg.get("headers", {}),
timeout=cfg.get("timeout", 5),
sse_read_timeout=cfg.get("sse_read_timeout", 60 * 5),
_http_client_kwargs["timeout"] = cfg.get("timeout", 5)
_http_client_kwargs["sse_read_timeout"] = cfg.get(
"sse_read_timeout", 60 * 5
)
self._streams_context = sse_client(**_http_client_kwargs)
streams = await self.exit_stack.enter_async_context(
self._streams_context,
)
Expand All @@ -461,17 +563,16 @@ def logging_callback(
),
)
else:
timeout = timedelta(seconds=cfg.get("timeout", 30))
sse_read_timeout = timedelta(
_http_client_kwargs["timeout"] = timedelta(
seconds=cfg.get("timeout", 30)
)
_http_client_kwargs["sse_read_timeout"] = timedelta(
seconds=cfg.get("sse_read_timeout", 60 * 5),
)
self._streams_context = streamablehttp_client(
url=cfg["url"],
headers=cfg.get("headers", {}),
timeout=timeout,
sse_read_timeout=sse_read_timeout,
terminate_on_close=cfg.get("terminate_on_close", True),
_http_client_kwargs["terminate_on_close"] = cfg.get(
"terminate_on_close", True
)
self._streams_context = streamablehttp_client(**_http_client_kwargs)
read_s, write_s, _ = await self.exit_stack.enter_async_context(
self._streams_context,
)
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/agent/runners/dify/dify_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ async def parse_file(item: dict):
case "video":
return Comp.Video(file=item["url"])
case _:
return Comp.File(name=item["filename"], file=item["url"])
return Comp.File(name=item["filename"], url=item["url"])

output = chunk["data"]["outputs"][self.workflow_output_key]
chains = []
Expand Down
Loading
Loading