Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
DawnARC
2026-05-02 21:46:04 +08:00
31 changed files with 366 additions and 185 deletions

View File

@@ -223,7 +223,7 @@ def is_mentioned_bot_in_message(message: SessionMessage) -> tuple[bool, bool, fl
break
# 7) 概率设置
if is_at and getattr(global_config.chat, "at_bot_inevitable_reply", 1):
if is_at and getattr(global_config.chat, "inevitable_at_reply", 1):
reply_probability = 1.0
logger.debug("被@回复概率设置为100%")
elif is_mentioned and getattr(global_config.chat, "mentioned_bot_reply", 1):

View File

@@ -57,7 +57,7 @@ MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute(
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
MMC_VERSION: str = "1.0.0"
CONFIG_VERSION: str = "8.9.20"
MODEL_CONFIG_VERSION: str = "1.14.3"
MODEL_CONFIG_VERSION: str = "1.14.5"
logger = get_logger("config")

View File

@@ -11,26 +11,29 @@ DEFAULT_PROVIDER_TEMPLATES: list[dict[str, Any]] = [
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"api_key": "your-api-key",
"auth_type": OpenAICompatibleAuthType.BEARER.value,
"max_retry": 3,
"timeout": 100,
"retry_interval": 8,
}
]
DEFAULT_TASK_CONFIG_TEMPLATES: dict[str, dict[str, Any]] = {
"utils": {
"model_list": ["qwen3.5-35b-a3b-nonthink"],
"model_list": ["deepseek-v4-flash"],
"max_tokens": 4096,
"temperature": 0.5,
"slow_threshold": 15.0,
"selection_strategy": "random",
},
"replyer": {
"model_list": ["ali-glm-5"],
"model_list": ["deepseek-v4-pro-think", "deepseek-v4-pro-nonthink"],
"max_tokens": 4096,
"temperature": 1,
"slow_threshold": 120.0,
"selection_strategy": "random",
},
"planner": {
"model_list": ["qwen3.5-35b-a3b", "qwen3.5-122b-a10b", "qwen3.5-flash"],
"model_list": ["deepseek-v4-flash"],
"max_tokens": 8000,
"temperature": 0.7,
"slow_threshold": 12.0,
@@ -61,40 +64,30 @@ DEFAULT_TASK_CONFIG_TEMPLATES: dict[str, dict[str, Any]] = {
DEFAULT_MODEL_TEMPLATES: list[dict[str, Any]] = [
{
"model_identifier": "glm-5",
"name": "ali-glm-5",
"model_identifier": "deepseek-v4-pro",
"name": "deepseek-v4-pro-think",
"api_provider": "BaiLian",
"price_in": 3.0,
"price_out": 14.0,
"temperature": 1.0,
"price_in": 12.0,
"price_out": 24.0,
"visual": False,
"extra_params": {"enable_thinking": False},
"extra_params": {"enable_thinking": "True"},
},
{
"model_identifier": "qwen3.5-122b-a10b",
"name": "qwen3.5-122b-a10b",
"model_identifier": "deepseek-v4-pro",
"name": "deepseek-v4-pro-nonthink",
"api_provider": "BaiLian",
"price_in": 0.8,
"price_out": 6.4,
"visual": True,
"price_in": 12.0,
"price_out": 24.0,
"visual": False,
"extra_params": {"enable_thinking": "false"},
},
{
"model_identifier": "qwen3.5-35b-a3b",
"name": "qwen3.5-35b-a3b",
"model_identifier": "deepseek-v4-flash",
"name": "deepseek-v4-flash",
"api_provider": "BaiLian",
"price_in": 0.4,
"price_out": 3.2,
"visual": True,
"extra_params": {},
},
{
"model_identifier": "qwen3.5-35b-a3b",
"name": "qwen3.5-35b-a3b-nonthink",
"api_provider": "BaiLian",
"price_in": 0.4,
"price_out": 3.2,
"visual": True,
"price_in": 1.0,
"price_out": 2.0,
"visual": False,
"extra_params": {"enable_thinking": "false"},
},
{

View File

@@ -172,7 +172,7 @@ class APIProvider(ConfigBase):
"""工具参数解析模式。可选值:`auto`、`strict`、`repair`、`double_decode`。"""
max_retry: int = Field(
default=2,
default=3,
ge=0,
json_schema_extra={
"x-widget": "input",
@@ -182,7 +182,7 @@ class APIProvider(ConfigBase):
"""最大重试次数 (单个模型API调用失败, 最多重试的次数)"""
timeout: int = Field(
default=10,
default=60,
ge=1,
json_schema_extra={
"x-widget": "input",
@@ -193,7 +193,7 @@ class APIProvider(ConfigBase):
"""API调用的超时时长 (超过这个时长, 本次请求将被视为"请求超时", 单位: 秒)"""
retry_interval: int = Field(
default=10,
default=5,
ge=1,
json_schema_extra={
"x-widget": "input",

View File

@@ -4,8 +4,6 @@ from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass, field
from typing import Any, Optional, TYPE_CHECKING
import random
from src.chat.message_receive.chat_manager import chat_manager
from src.cli.maisaka_cli_sender import CLI_PLATFORM_NAME, render_cli_message
from src.common.data_models.image_data_model import MaiEmoji
@@ -121,45 +119,13 @@ def _normalize_emotions(emoji: MaiEmoji) -> list[str]:
return []
async def select_emoji_for_maisaka(
*,
requested_emotion: str = "",
reasoning: str = "",
context_texts: Sequence[str] | None = None,
sample_size: int = 30,
) -> tuple[MaiEmoji | None, str]:
"""为 Maisaka 选择一个合适的表情。"""
del reasoning, context_texts
available_emojis = list(emoji_manager.emojis)
if not available_emojis:
return None, ""
normalized_requested_emotion = requested_emotion.strip()
if normalized_requested_emotion:
matched_emojis = [
emoji
for emoji in available_emojis
if normalized_requested_emotion.lower() in (emotion.lower() for emotion in _normalize_emotions(emoji))
]
if matched_emojis:
return random.choice(matched_emojis), normalized_requested_emotion
sampled_emojis = random.sample(
available_emojis,
min(max(sample_size, 1), len(available_emojis)),
)
return random.choice(sampled_emojis), ""
async def send_emoji_for_maisaka(
*,
stream_id: str,
emoji_selector: EmojiSelector,
requested_emotion: str = "",
reasoning: str = "",
context_texts: Sequence[str] | None = None,
emoji_selector: EmojiSelector | None = None,
) -> MaisakaEmojiSendResult:
"""为 Maisaka 选择并发送一个表情。"""
@@ -194,20 +160,12 @@ async def send_emoji_for_maisaka(
normalized_context_texts = _normalize_context_texts(before_select_kwargs.get("context_texts"))
sample_size = _coerce_positive_int(before_select_kwargs.get("sample_size"), sample_size)
if emoji_selector is None:
selected_emoji, matched_emotion = await select_emoji_for_maisaka(
requested_emotion=normalized_requested_emotion,
reasoning=normalized_reasoning,
context_texts=normalized_context_texts,
sample_size=sample_size,
)
else:
selected_emoji, matched_emotion = await emoji_selector(
normalized_requested_emotion,
normalized_reasoning,
normalized_context_texts,
sample_size,
)
selected_emoji, matched_emotion = await emoji_selector(
normalized_requested_emotion,
normalized_reasoning,
normalized_context_texts,
sample_size,
)
after_select_result = await _get_runtime_manager().invoke_hook(
"emoji.maisaka.after_select",
stream_id=stream_id,

View File

@@ -2,6 +2,7 @@
from datetime import datetime
from io import BytesIO
from json import dumps
from random import sample
from typing import Any, Dict, Optional
@@ -17,9 +18,8 @@ from src.emoji_system.maisaka_tool import send_emoji_for_maisaka
from src.common.data_models.image_data_model import MaiEmoji
from src.common.data_models.message_component_data_model import ImageComponent, MessageSequence, TextComponent
from src.common.logger import get_logger
from src.config.config import global_config
from src.config.config import config_manager, global_config
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
from src.llm_models.payload_content.resp_format import RespFormat, RespFormatType
from src.llm_models.payload_content.message import MessageBuilder, RoleType
from src.maisaka.context_messages import (
LLMContextMessage,
@@ -221,6 +221,7 @@ def _build_send_emoji_monitor_detail(
detail: Dict[str, Any] = {}
if isinstance(request_messages, list) and request_messages:
detail["request_messages"] = request_messages
detail["prompt_text"] = dumps(request_messages, ensure_ascii=False, indent=2)
if reasoning_text.strip():
detail["reasoning_text"] = reasoning_text.strip()
if output_text.strip():
@@ -279,6 +280,24 @@ def _build_send_emoji_monitor_metadata(
return {}
def _resolve_emoji_selector_model_task_name() -> str:
"""根据 planner 模型视觉能力选择表情选择子代理的模型任务。"""
model_config = config_manager.get_model_config()
planner_models = [
model_name
for model_name in model_config.model_task_config.planner.model_list
if str(model_name).strip()
]
models_by_name = {model.name: model for model in model_config.models}
if planner_models and all(
model_name in models_by_name and models_by_name[model_name].visual
for model_name in planner_models
):
return "planner"
return "vlm"
async def _select_emoji_with_sub_agent(
tool_ctx: BuiltinToolRuntimeContext,
reasoning: str,
@@ -326,7 +345,8 @@ async def _select_emoji_with_sub_agent(
prompt_llm_message = prompt_message.to_llm_message()
if prompt_llm_message is not None:
request_messages.append(prompt_llm_message)
candidate_llm_message = candidate_message.to_llm_message()
candidate_to_llm_message = getattr(candidate_message, "to_llm_message", None)
candidate_llm_message = candidate_to_llm_message() if callable(candidate_to_llm_message) else None
if candidate_llm_message is not None:
request_messages.append(candidate_llm_message)
serialized_request_messages = serialize_prompt_messages(request_messages)
@@ -337,10 +357,7 @@ async def _select_emoji_with_sub_agent(
system_prompt=system_prompt,
extra_messages=[prompt_message, candidate_message],
max_tokens=_EMOJI_SUB_AGENT_MAX_TOKENS,
response_format=RespFormat(
format_type=RespFormatType.JSON_SCHEMA,
schema=EmojiSelectionResult,
),
model_task_name=_resolve_emoji_selector_model_task_name(),
)
selection_duration_ms = round((datetime.now() - selection_started_at).total_seconds() * 1000, 2)
@@ -409,12 +426,16 @@ async def handle_tool(
"reason": "",
}
selection_metadata: Dict[str, Any] = {"reason": "", "monitor_detail": {}}
requested_emotion = ""
if isinstance(invocation.arguments, dict):
requested_emotion = str(invocation.arguments.get("emotion") or "").strip()
logger.info(f"{tool_ctx.runtime.log_prefix} 触发表情包发送工具")
try:
send_result = await send_emoji_for_maisaka(
stream_id=tool_ctx.runtime.session_id,
requested_emotion=requested_emotion,
reasoning=tool_ctx.engine.last_reasoning_content,
context_texts=context_texts,
emoji_selector=lambda _requested_emotion, reasoning, context_texts, sample_size: _select_emoji_with_sub_agent(

View File

@@ -194,6 +194,7 @@ class MaisakaChatLoopService:
session_id: Optional[str] = None,
is_group_chat: Optional[bool] = None,
max_tokens: int = 2048,
model_task_name: str = "planner",
) -> None:
"""初始化 Maisaka 对话循环服务。
@@ -205,6 +206,7 @@ class MaisakaChatLoopService:
"""
self._max_tokens = max_tokens
self._model_task_name = model_task_name.strip() or "planner"
self._is_group_chat = is_group_chat
self._session_id = session_id or ""
self._extra_tools: List[ToolOption] = []
@@ -236,17 +238,18 @@ class MaisakaChatLoopService:
)
def _get_llm_chat_client(self, request_kind: str) -> LLMServiceClient:
"""获取当前请求类型对应的 planner LLM 客户端。"""
"""获取当前请求类型对应的 LLM 客户端。"""
request_type = self._resolve_llm_request_type(request_kind)
llm_client = self._llm_chat_clients.get(request_type)
client_key = f"{self._model_task_name}:{request_type}"
llm_client = self._llm_chat_clients.get(client_key)
if llm_client is None:
llm_client = LLMServiceClient(
task_name="planner",
task_name=self._model_task_name,
request_type=request_type,
session_id=self._session_id,
)
self._llm_chat_clients[request_type] = llm_client
self._llm_chat_clients[client_key] = llm_client
return llm_client
@staticmethod

View File

@@ -473,13 +473,18 @@ class MaisakaHeartFlowChatting:
def _update_message_trigger_state(self, message: SessionMessage) -> None:
"""补齐消息中的 @/提及 标记,并在命中时启用强制 continue。"""
detected_mentioned, detected_at, _ = is_mentioned_bot_in_message(message)
detected_mentioned, detected_at, reply_probability_boost = is_mentioned_bot_in_message(message)
if detected_at:
message.is_at = True
if detected_mentioned:
message.is_mentioned = True
if not message.is_at and not message.is_mentioned:
should_force_reply = (
reply_probability_boost >= 1.0
or (message.is_at and global_config.chat.inevitable_at_reply)
or (message.is_mentioned and global_config.chat.mentioned_bot_reply)
)
if not should_force_reply or (not message.is_at and not message.is_mentioned):
return
self._arm_force_next_timing_continue(
@@ -537,6 +542,11 @@ class MaisakaHeartFlowChatting:
self._force_next_timing_reason = ""
return reason
def _has_forced_timing_trigger(self) -> bool:
"""判断是否已有 @/提及必回触发,需绕过普通频率阈值。"""
return self._force_next_timing_continue
def _bind_planner_interrupt_flag(self, interrupt_flag: asyncio.Event) -> None:
"""绑定当前可打断请求使用的中断标记。"""
self._planner_interrupt_flag = interrupt_flag
@@ -590,6 +600,7 @@ class MaisakaHeartFlowChatting:
extra_messages: Optional[Sequence[LLMContextMessage]] = None,
interrupt_flag: asyncio.Event | None = None,
max_tokens: int = 512,
model_task_name: str = "planner",
response_format: RespFormat | None = None,
tool_definitions: Optional[Sequence[ToolDefinitionInput]] = None,
) -> ChatResponse:
@@ -603,6 +614,7 @@ class MaisakaHeartFlowChatting:
sub_agent_history = self._drop_head_context_messages(
selected_history,
drop_head_context_count,
trim_threshold_context_count=context_message_limit,
)
if extra_messages:
sub_agent_history.extend(list(extra_messages))
@@ -612,6 +624,7 @@ class MaisakaHeartFlowChatting:
session_id=self.session_id,
is_group_chat=self.chat_stream.is_group_session,
max_tokens=max_tokens,
model_task_name=model_task_name,
)
sub_agent.set_interrupt_flag(interrupt_flag)
return await sub_agent.chat_loop_step(
@@ -625,12 +638,21 @@ class MaisakaHeartFlowChatting:
def _drop_head_context_messages(
chat_history: Sequence[LLMContextMessage],
drop_context_count: int,
*,
trim_threshold_context_count: int | None = None,
) -> list[LLMContextMessage]:
"""从已选上下文头部丢弃指定数量的普通上下文消息。"""
if drop_context_count <= 0:
return list(chat_history)
context_message_count = sum(1 for message in chat_history if message.count_in_context)
if trim_threshold_context_count is not None and context_message_count <= trim_threshold_context_count:
return list(chat_history)
if context_message_count <= drop_context_count:
return list(chat_history)
first_kept_index = 0
dropped_context_count = 0
while (
@@ -867,6 +889,12 @@ class MaisakaHeartFlowChatting:
if pending_count <= 0:
return
if self._has_forced_timing_trigger():
self._cancel_deferred_message_turn_task()
self._message_turn_scheduled = True
self._internal_turn_queue.put_nowait("message")
return
trigger_threshold = self._get_message_trigger_threshold()
if pending_count >= trigger_threshold or self._should_trigger_message_turn_by_idle_compensation(
pending_count=pending_count,

View File

@@ -146,6 +146,11 @@ async def _fetch_models_from_provider(
client_config = build_openai_compatible_client_config(provider)
headers.update(client_config.default_headers)
params.update(client_config.default_query)
# build_openai_compatible_client_config 在“默认 Bearer”场景下
# 会把 api_key 留在 client_config.api_key 中交给 OpenAI SDK 自行注入 Authorization 头,
# 而不会写入 default_headers。这里我们用 httpx 直接发请求,需要手动补上鉴权头/参数。
if client_config.api_key and "Authorization" not in headers:
headers["Authorization"] = f"Bearer {client_config.api_key}"
try:
async with httpx.AsyncClient(timeout=30.0) as client: