Skip to content

Commit 40ce239

Browse files
authored
Merge branch 'AstrBotDevs:master' into master
2 parents 49be025 + dee4f14 commit 40ce239

49 files changed

Lines changed: 1129 additions & 319 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

astrbot/core/astr_main_agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ class MainAgentBuildConfig:
153153
This enforce max turns before compression"""
154154
dequeue_context_length: int = 1
155155
"""The number of oldest turns to remove when context length limit is reached."""
156+
fallback_max_context_tokens: int = 128000
157+
"""Fallback max context tokens. When max_context_tokens is 0 and the model is not in LLM_METADATAS, use this value."""
156158
llm_safety_mode: bool = True
157159
"""This will inject healthy and safe system prompt into the main agent,
158160
to prevent LLM output harmful information"""
@@ -1465,6 +1467,11 @@ async def build_main_agent(
14651467
provider.provider_config["max_context_tokens"] = model_info["limit"][
14661468
"context"
14671469
]
1470+
else:
1471+
# fallback: default to configured fallback value
1472+
provider.provider_config["max_context_tokens"] = (
1473+
config.fallback_max_context_tokens
1474+
)
14681475

14691476
if event.get_platform_name() == "webchat":
14701477
asyncio.create_task(_handle_webchat(event, req, provider))

astrbot/core/config/default.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。"""
22

33
import os
4-
from typing import Any, TypedDict
54

65
from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG
76
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
@@ -316,24 +315,6 @@
316315
}
317316

318317

319-
class ChatProviderTemplate(TypedDict):
320-
id: str
321-
provider_source_id: str
322-
model: str
323-
modalities: list
324-
custom_extra_body: dict[str, Any]
325-
max_context_tokens: int
326-
327-
328-
CHAT_PROVIDER_TEMPLATE = {
329-
"id": "",
330-
"provide_source_id": "",
331-
"model": "",
332-
"modalities": [],
333-
"custom_extra_body": {},
334-
"max_context_tokens": 0,
335-
}
336-
337318
"""
338319
AstrBot v3 时代的配置元数据,目前仅承担以下功能:
339320
@@ -1992,13 +1973,13 @@ class ChatProviderTemplate(TypedDict):
19921973
"options": ["text", "image", "audio", "tool_use"],
19931974
"labels": ["文本", "图像", "音频", "工具使用"],
19941975
"render_type": "checkbox",
1995-
"hint": "模型支持的模态。如所填写的模型不支持图像,请取消勾选图像。",
1976+
"hint": "模型支持的模态及能力。",
19961977
},
19971978
"custom_headers": {
1998-
"description": "自定义添加请求头",
1979+
"description": "自定义请求头",
19991980
"type": "dict",
20001981
"items": {},
2001-
"hint": "此处添加的键值对将被合并到 OpenAI SDK 的 default_headers 中,用于自定义 HTTP 请求头。值必须为字符串。",
1982+
"hint": "此处添加的键值对将被合并到 OpenAI SDK 的 default_headers 中,用于自定义 HTTP 请求头。",
20021983
},
20031984
"ollama_disable_thinking": {
20041985
"description": "关闭思考模式",
@@ -2009,7 +1990,7 @@ class ChatProviderTemplate(TypedDict):
20091990
"description": "自定义请求体参数",
20101991
"type": "dict",
20111992
"items": {},
2012-
"hint": "用于在请求时添加额外的参数,如 temperaturetop_pmax_tokens 等。",
1993+
"hint": "用于在请求时添加额外的参数,如 temperature, top_p, max_tokens, reasoning_effort 等。",
20131994
"template_schema": {
20141995
"temperature": {
20151996
"name": "Temperature",
@@ -2652,7 +2633,7 @@ class ChatProviderTemplate(TypedDict):
26522633
"max_context_tokens": {
26532634
"description": "模型上下文窗口大小",
26542635
"type": "int",
2655-
"hint": "模型最大上下文 Token 大小。如果为 0,则会自动从模型元数据填充(如有),也可手动修改。",
2636+
"hint": "模型最大上下文 Token 大小。如果为 0,则会自动从模型元数据填充(如有)",
26562637
},
26572638
"dify_api_key": {
26582639
"description": "API Key",
@@ -3566,6 +3547,14 @@ class ChatProviderTemplate(TypedDict):
35663547
"provider_settings.agent_runner_type": "local",
35673548
},
35683549
},
3550+
"provider_settings.fallback_max_context_tokens": {
3551+
"description": "上下文窗口兜底值",
3552+
"type": "int",
3553+
"hint": "当 max_context_tokens 为 0 且模型不在内置元数据中时,使用此值作为上下文窗口大小。默认 128000。",
3554+
"condition": {
3555+
"provider_settings.agent_runner_type": "local",
3556+
},
3557+
},
35693558
},
35703559
"condition": {
35713560
"provider_settings.agent_runner_type": "local",

astrbot/core/knowledge_base/kb_mgr.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ def __init__(
3636
async def initialize(self) -> None:
3737
"""初始化知识库模块"""
3838
try:
39-
logger.info("正在初始化知识库模块...")
40-
4139
# 初始化数据库
4240
await self._init_kb_database()
4341

astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ async def initialize(self, ctx: PipelineContext) -> None:
107107
)
108108
if self.dequeue_context_length <= 0:
109109
self.dequeue_context_length = 1
110+
self.fallback_max_context_tokens: int = settings.get(
111+
"fallback_max_context_tokens", 128000
112+
)
110113

111114
self.llm_safety_mode = settings.get("llm_safety_mode", True)
112115
self.safety_mode_strategy = settings.get(
@@ -136,6 +139,7 @@ async def initialize(self, ctx: PipelineContext) -> None:
136139
llm_compress_provider_id=self.llm_compress_provider_id,
137140
max_context_length=self.max_context_length,
138141
dequeue_context_length=self.dequeue_context_length,
142+
fallback_max_context_tokens=self.fallback_max_context_tokens,
139143
llm_safety_mode=self.llm_safety_mode,
140144
safety_mode_strategy=self.safety_mode_strategy,
141145
computer_use_runtime=self.computer_use_runtime,

astrbot/core/platform/astr_message_event.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def __init__(
5353
self.is_at_or_wake_command = False
5454
"""是否是 At 机器人或者带有唤醒词或者是私聊(插件注册的事件监听器会让 is_wake 设为 True, 但是不会让这个属性置为 True)"""
5555
self._extras: dict[str, Any] = {}
56+
self._force_stopped: bool = False
57+
"""独立的停止标志,不依赖 _result,不会被 clear_result() 重置"""
5658
message_type = getattr(message_obj, "type", None)
5759
if not isinstance(message_type, MessageType):
5860
try:
@@ -336,20 +338,24 @@ async def check_count(self, event: AstrMessageEvent):
336338

337339
def stop_event(self) -> None:
338340
"""终止事件传播。"""
341+
self._force_stopped = True
339342
if self._result is None:
340343
self.set_result(MessageEventResult().stop_event())
341344
else:
342345
self._result.stop_event()
343346

344347
def continue_event(self) -> None:
345348
"""继续事件传播。"""
349+
self._force_stopped = False
346350
if self._result is None:
347351
self.set_result(MessageEventResult().continue_event())
348352
else:
349353
self._result.continue_event()
350354

351355
def is_stopped(self) -> bool:
352356
"""是否终止事件传播。"""
357+
if self._force_stopped:
358+
return True
353359
if self._result is None:
354360
return False # 默认是继续传播
355361
return self._result.is_stopped()

astrbot/core/platform/platform.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
import asyncio
23
import uuid
34
from asyncio import Queue
45
from collections.abc import Coroutine
@@ -138,7 +139,9 @@ async def send_by_session(
138139
139140
异步方法。
140141
"""
141-
await Metric.upload(msg_event_tick=1, adapter_name=self.meta().name)
142+
asyncio.create_task(
143+
Metric.upload(msg_event_tick=1, adapter_name=self.meta().name)
144+
)
142145

143146
def commit_event(self, event: AstrMessageEvent) -> None:
144147
"""提交一个事件到事件队列。"""

astrbot/core/platform/sources/lark/lark_event.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,9 @@ async def _fallback_send_streaming(self, generator, use_fallback: bool = False):
949949
buffer.squash_plain()
950950
await self.send(buffer)
951951

952-
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
952+
asyncio.create_task(
953+
Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
954+
)
953955
self._has_send_oper = True
954956

955957
async def send_streaming(self, generator, use_fallback: bool = False):
@@ -1000,7 +1002,9 @@ async def _consume_rest_and_fallback(gen, initial_text: str) -> None:
10001002
if buffer:
10011003
buffer.squash_plain()
10021004
await self.send(buffer)
1003-
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
1005+
asyncio.create_task(
1006+
Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
1007+
)
10041008
self._has_send_oper = True
10051009

10061010
async def _flush_and_close_card() -> None:
@@ -1075,14 +1079,18 @@ async def _flush_and_close_card() -> None:
10751079
# If no text was produced at all, no card was created
10761080
if card_id is None:
10771081
if not fallback_used:
1078-
await Metric.upload(
1079-
msg_event_tick=1, adapter_name=self.platform_meta.name
1082+
asyncio.create_task(
1083+
Metric.upload(
1084+
msg_event_tick=1, adapter_name=self.platform_meta.name
1085+
)
10801086
)
10811087
self._has_send_oper = True
10821088
return
10831089

10841090
await _flush_and_close_card()
10851091

10861092
# 内联父类 send_streaming 的副作用
1087-
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
1093+
asyncio.create_task(
1094+
Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
1095+
)
10881096
self._has_send_oper = True

astrbot/core/platform/sources/wecom/wecom_adapter.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import time
55
import uuid
66
from collections.abc import Awaitable, Callable
7+
from pathlib import Path
78
from typing import Any, cast
9+
from urllib.parse import unquote
810

911
import quart
1012
from requests import Response
@@ -15,7 +17,7 @@
1517
from wechatpy.messages import BaseMessage
1618

1719
from astrbot.api.event import MessageChain
18-
from astrbot.api.message_components import Image, Plain, Record
20+
from astrbot.api.message_components import File, Image, Plain, Record
1921
from astrbot.api.platform import (
2022
AstrBotMessage,
2123
MessageMember,
@@ -40,6 +42,27 @@
4042
from typing_extensions import override
4143

4244

45+
def _extract_wecom_media_filename(disposition: str | None) -> str | None:
46+
if not disposition:
47+
return None
48+
49+
for part in disposition.split(";"):
50+
token = part.strip()
51+
token_lower = token.lower()
52+
if token_lower.startswith("filename*="):
53+
value = token.split("=", 1)[1].strip().strip('"')
54+
if value.lower().startswith("utf-8''"):
55+
value = value[7:]
56+
filename = Path(unquote(value).replace("\\", "/")).name
57+
return filename or None
58+
if token_lower.startswith("filename="):
59+
value = token.split("=", 1)[1].strip().strip('"')
60+
filename = Path(value.replace("\\", "/")).name
61+
return filename or None
62+
63+
return None
64+
65+
4366
class WecomServer:
4467
def __init__(self, event_queue: asyncio.Queue, config: dict) -> None:
4568
self.server = quart.Quart(__name__)
@@ -459,6 +482,29 @@ async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None:
459482
return
460483

461484
abm.message = [Record(file=path_wav, url=path_wav)]
485+
elif msgtype == "file":
486+
media_id = msg.get("file", {}).get("media_id", "")
487+
if not media_id:
488+
logger.warning(f"微信客服文件消息缺少 media_id: {msg}")
489+
return
490+
491+
resp: Response = await asyncio.get_running_loop().run_in_executor(
492+
None,
493+
self.client.media.download,
494+
media_id,
495+
)
496+
497+
file_name = (
498+
_extract_wecom_media_filename(
499+
resp.headers.get("Content-Disposition"),
500+
)
501+
or f"weixinkefu_{media_id}.bin"
502+
)
503+
temp_dir = Path(get_astrbot_temp_path())
504+
file_path = temp_dir / f"weixinkefu_{uuid.uuid4().hex}_{file_name}"
505+
file_path.write_bytes(resp.content)
506+
507+
abm.message = [File(name=file_name, file=str(file_path))]
462508
else:
463509
logger.warning(f"未实现的微信客服消息事件: {msg}")
464510
return

astrbot/core/provider/func_tool_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ def add_func(
311311
handler=handler,
312312
),
313313
)
314-
logger.info(f"添加函数调用工具: {name}")
314+
logger.info(f"Added llm tool: {name}")
315315

316316
def remove_func(self, name: str) -> None:
317317
"""删除一个函数调用工具。"""

astrbot/core/provider/sources/anthropic_source.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import base64
22
import json
33
from collections.abc import AsyncGenerator
4-
from typing import Literal
4+
from typing import Any, Literal
55

66
import anthropic
77
import httpx
@@ -104,15 +104,35 @@ def _init_api_key(self, provider_config: dict) -> None:
104104
api_key=self.chosen_api_key,
105105
timeout=self.timeout,
106106
base_url=self.base_url,
107+
default_headers=self.custom_headers,
107108
http_client=self._create_http_client(provider_config),
108109
)
109110

110-
def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient:
111-
"""创建带代理的 HTTP 客户端,使用系统 SSL 证书"""
111+
def _create_http_client(self, provider_config: dict) -> httpx.AsyncClient | None:
112+
"""Create an HTTP client with optional proxy and system SSL trust store.
113+
114+
The Anthropic SDK validates ``http_client`` with
115+
``isinstance(..., httpx.AsyncClient)`` against its own ``httpx`` import.
116+
When multiple ``httpx`` installations are present on ``sys.path``
117+
(e.g. bundled Python + system Python), constructing the client from a
118+
different ``httpx`` module makes that check fail. We therefore prefer
119+
the SDK's own ``httpx`` module when available.
120+
"""
121+
proxy = provider_config.get("proxy", "")
122+
if not proxy:
123+
return None
124+
httpx_module: Any = httpx
125+
try:
126+
from anthropic import _base_client as anthropic_base_client
127+
128+
httpx_module = getattr(anthropic_base_client, "httpx", httpx)
129+
except ImportError:
130+
pass
112131
return create_proxy_client(
113132
"Anthropic",
114-
provider_config.get("proxy", ""),
133+
proxy,
115134
headers=self.custom_headers,
135+
httpx_module=httpx_module,
116136
)
117137

118138
def _apply_thinking_config(self, payloads: dict) -> None:
@@ -591,7 +611,11 @@ async def text_chat(
591611

592612
# Anthropic has a different way of handling system prompts
593613
if system_prompt:
594-
payloads["system"] = system_prompt
614+
payloads["system"] = (
615+
[{"type": "text", "text": system_prompt}]
616+
if isinstance(system_prompt, str)
617+
else system_prompt
618+
)
595619

596620
llm_response = None
597621
try:
@@ -654,7 +678,11 @@ async def text_chat_stream(
654678

655679
# Anthropic has a different way of handling system prompts
656680
if system_prompt:
657-
payloads["system"] = system_prompt
681+
payloads["system"] = (
682+
[{"type": "text", "text": system_prompt}]
683+
if isinstance(system_prompt, str)
684+
else system_prompt
685+
)
658686

659687
async for llm_response in self._query_stream(payloads, func_tool):
660688
yield llm_response

0 commit comments

Comments
 (0)