本次提交对蓝泡泡适配器进行了多维度改进: 1. 新增缓存超限自动清理逻辑,防止内存溢出 2. 增强流式打字同步功能,增加超时自动停止逻辑 3. 重构markdown渲染逻辑,修复格式匹配问题并支持更多语法 4. 新增配置校验工具函数,增加streaming_mode和auth_strategy的合法性校验 5. 新增多项环境配置参数,完善配置项 6. 重构初始化流程,新增多个工具类实例 7. 优化TTS禁用逻辑,新增aiohttp依赖检查 8. 重构流式消息发送逻辑,整合打字指示器逻辑 9. 新增健康状态检查的问题检测功能
214 lines
8.8 KiB
Python
214 lines
8.8 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
class CatchupConfig(BaseModel):
|
|
enabled: bool = True
|
|
max_age_minutes: int = 120
|
|
per_run_limit: int = 50
|
|
first_run_lookback_minutes: int = 30
|
|
max_failure_retries: int = 10
|
|
|
|
|
|
class HealthMonitorConfig(BaseModel):
|
|
enabled: bool = True
|
|
|
|
|
|
class NetworkConfig(BaseModel):
|
|
dangerously_allow_private_network: bool = False
|
|
|
|
|
|
class MarkdownConfig(BaseModel):
|
|
enabled: bool = True
|
|
code_blocks: bool = True
|
|
|
|
|
|
class BlockStreamingCoalesceConfig(BaseModel):
|
|
enabled: bool = False
|
|
max_flush_interval_ms: int = 500
|
|
|
|
|
|
class GroupOverride(BaseModel):
|
|
system_prompt: str = ""
|
|
|
|
|
|
class BlueBubblesConfig(BaseModel):
|
|
enabled: bool = False
|
|
name: str = ""
|
|
server_url: str = Field(default="http://localhost:1234")
|
|
password: str = Field(default="")
|
|
request_timeout: float = 30.0
|
|
send_timeout_ms: int = 30000
|
|
reconnect_max_delay: float = 60.0
|
|
|
|
dm_policy: Literal["open", "pairing", "allowlist", "blocklist"] = "pairing"
|
|
group_policy: Literal["open", "pairing", "allowlist", "blocklist"] = "allowlist"
|
|
dm_allow_from: list[str] = []
|
|
group_allow_chats: list[str] = []
|
|
group_allow_from: list[str] = []
|
|
|
|
media_max_mb: int = 100
|
|
enable_stickers: bool = True
|
|
media_local_roots: list[str] = []
|
|
|
|
streaming_mode: Literal["off", "partial", "block"] = "partial"
|
|
edit_interval_ms: int = 500
|
|
block_streaming: bool = False
|
|
block_streaming_coalesce: BlockStreamingCoalesceConfig = Field(default_factory=BlockStreamingCoalesceConfig)
|
|
|
|
text_chunk_limit: int = 4000
|
|
chunk_mode: Literal["length", "newline"] = "newline"
|
|
|
|
tapback_enabled: bool = True
|
|
require_mention: bool = False
|
|
|
|
ai_vision_enabled: bool = False
|
|
streaming_typing_indicator: bool = True
|
|
message_order_check: bool = True
|
|
max_cache_entries: int = 2048
|
|
|
|
send_read_receipts: bool = True
|
|
|
|
dm_history_limit: int = 100
|
|
|
|
coalesce_same_sender_dms: bool = False
|
|
|
|
enrich_group_participants_from_contacts: bool = True
|
|
|
|
allow_private_network: bool = False
|
|
network: NetworkConfig = Field(default_factory=NetworkConfig)
|
|
|
|
config_writes: bool = True
|
|
|
|
webhook_path: str = "/bluebubbles/webhook"
|
|
webhook_secret: str = ""
|
|
|
|
markdown: MarkdownConfig = Field(default_factory=MarkdownConfig)
|
|
|
|
capabilities: list[str] = []
|
|
|
|
dms: dict[str, dict[str, Any]] = {}
|
|
default_account: str = ""
|
|
|
|
health_monitor: HealthMonitorConfig = Field(default_factory=HealthMonitorConfig)
|
|
|
|
catchup: CatchupConfig = Field(default_factory=CatchupConfig)
|
|
|
|
groups: dict[str, GroupOverride] = {}
|
|
|
|
auth_strategy: Literal["header", "query", "both"] = "header"
|
|
auth_header_name: str = "X-BB-Password"
|
|
tts_prefer_caf: bool = True
|
|
|
|
def __repr__(self) -> str:
|
|
d = self.model_dump()
|
|
if d.get("password"):
|
|
d["password"] = "***"
|
|
fields = ", ".join(f"{k}={v!r}" for k, v in d.items())
|
|
return f"{self.__class__.__name__}({fields})"
|
|
|
|
@field_validator("allow_private_network", mode="before")
|
|
@classmethod
|
|
def _deprecate_allow_private_network(cls, v, info) -> Any:
|
|
if v is True:
|
|
logger.warning(
|
|
"[BlueBubbles] 'allow_private_network' is deprecated, "
|
|
"use 'network.dangerously_allow_private_network' instead"
|
|
)
|
|
return v
|
|
|
|
@classmethod
|
|
def _validate_literal(cls, value: str, valid_values: list[str], field_name: str, default: str) -> str:
|
|
if value not in valid_values:
|
|
logger.warning(
|
|
"[BlueBubbles] Invalid value '%s' for '%s', falling back to '%s'. Valid values: %s",
|
|
value,
|
|
field_name,
|
|
default,
|
|
valid_values,
|
|
)
|
|
return default
|
|
return value
|
|
|
|
@classmethod
|
|
def from_env(cls) -> BlueBubblesConfig:
|
|
dm_raw = os.getenv("BLUEBUBBLES_DM_ALLOW_FROM", "")
|
|
group_raw = os.getenv("BLUEBUBBLES_GROUP_ALLOW_CHATS", "")
|
|
media_roots_raw = os.getenv("BLUEBUBBLES_MEDIA_LOCAL_ROOTS", "")
|
|
|
|
return cls(
|
|
enabled=os.getenv("BLUEBUBBLES_ENABLED", "false").lower() == "true",
|
|
name=os.getenv("BLUEBUBBLES_NAME", ""),
|
|
server_url=os.getenv("BLUEBUBBLES_SERVER_URL", "http://localhost:1234"),
|
|
password=os.getenv("BLUEBUBBLES_SERVER_PASSWORD", ""),
|
|
request_timeout=float(os.getenv("BLUEBUBBLES_REQUEST_TIMEOUT", "30.0")),
|
|
send_timeout_ms=int(os.getenv("BLUEBUBBLES_SEND_TIMEOUT_MS", "30000")),
|
|
reconnect_max_delay=float(os.getenv("BLUEBUBBLES_RECONNECT_MAX_DELAY", "60.0")),
|
|
dm_policy=os.getenv("BLUEBUBBLES_DM_POLICY", "pairing"),
|
|
group_policy=os.getenv("BLUEBUBBLES_GROUP_POLICY", "allowlist"),
|
|
dm_allow_from=[x.strip() for x in dm_raw.split(",") if x.strip()] if dm_raw else [],
|
|
group_allow_chats=[x.strip() for x in group_raw.split(",") if x.strip()] if group_raw else [],
|
|
media_max_mb=int(os.getenv("BLUEBUBBLES_MEDIA_MAX_MB", "100")),
|
|
enable_stickers=os.getenv("BLUEBUBBLES_ENABLE_STICKERS", "true").lower() == "true",
|
|
media_local_roots=[x.strip() for x in media_roots_raw.split(",") if x.strip()] if media_roots_raw else [],
|
|
streaming_mode=cls._validate_literal(
|
|
os.getenv("BLUEBUBBLES_STREAMING_MODE", "partial"),
|
|
["off", "partial", "block"],
|
|
"streaming_mode",
|
|
"partial",
|
|
),
|
|
edit_interval_ms=int(os.getenv("BLUEBUBBLES_EDIT_INTERVAL_MS", "500")),
|
|
tapback_enabled=os.getenv("BLUEBUBBLES_TAPBACK_ENABLED", "true").lower() == "true",
|
|
require_mention=os.getenv("BLUEBUBBLES_REQUIRE_MENTION", "false").lower() == "true",
|
|
send_read_receipts=os.getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true").lower() == "true",
|
|
text_chunk_limit=int(os.getenv("BLUEBUBBLES_TEXT_CHUNK_LIMIT", "4000")),
|
|
block_streaming=os.getenv("BLUEBUBBLES_BLOCK_STREAMING", "false").lower() == "true",
|
|
allow_private_network=os.getenv("BLUEBUBBLES_ALLOW_PRIVATE_NETWORK", "false").lower() == "true",
|
|
webhook_path=os.getenv("BLUEBUBBLES_WEBHOOK_PATH", "/bluebubbles/webhook"),
|
|
webhook_secret=os.getenv("BLUEBUBBLES_WEBHOOK_SECRET", ""),
|
|
auth_strategy=cls._validate_literal(
|
|
os.getenv("BLUEBUBBLES_AUTH_STRATEGY", "header"),
|
|
["header", "query", "both"],
|
|
"auth_strategy",
|
|
"header",
|
|
),
|
|
tts_prefer_caf=os.getenv("BLUEBUBBLES_TTS_PREFER_CAF", "true").lower() == "true",
|
|
streaming_typing_indicator=os.getenv("BLUEBUBBLES_STREAMING_TYPING", "true").lower() == "true",
|
|
message_order_check=os.getenv("BLUEBUBBLES_MESSAGE_ORDER_CHECK", "true").lower() == "true",
|
|
max_cache_entries=int(os.getenv("BLUEBUBBLES_MAX_CACHE_ENTRIES", "2048")),
|
|
dm_history_limit=int(os.getenv("BLUEBUBBLES_DM_HISTORY_LIMIT", "100")),
|
|
coalesce_same_sender_dms=os.getenv("BLUEBUBBLES_COALESCE_DMS", "false").lower() == "true",
|
|
enrich_group_participants_from_contacts=os.getenv("BLUEBUBBLES_ENRICH_GROUP", "true").lower() == "true",
|
|
config_writes=os.getenv("BLUEBUBBLES_CONFIG_WRITES", "true").lower() == "true",
|
|
block_streaming_coalesce=BlockStreamingCoalesceConfig(
|
|
enabled=os.getenv("BLUEBUBBLES_COALESCE_ENABLED", "false").lower() == "true",
|
|
max_flush_interval_ms=int(os.getenv("BLUEBUBBLES_COALESCE_MAX_FLUSH_MS", "500")),
|
|
),
|
|
network=NetworkConfig(
|
|
dangerously_allow_private_network=os.getenv(
|
|
"BLUEBUBBLES_NETWORK_DANGEROUSLY_ALLOW_PRIVATE", "false"
|
|
).lower()
|
|
== "true",
|
|
),
|
|
markdown=MarkdownConfig(
|
|
enabled=os.getenv("BLUEBUBBLES_MARKDOWN_ENABLED", "true").lower() == "true",
|
|
code_blocks=os.getenv("BLUEBUBBLES_MARKDOWN_CODE_BLOCKS", "true").lower() == "true",
|
|
),
|
|
catchup=CatchupConfig(
|
|
enabled=os.getenv("BLUEBUBBLES_CATCHUP_ENABLED", "true").lower() == "true",
|
|
max_age_minutes=int(os.getenv("BLUEBUBBLES_CATCHUP_MAX_AGE_MINUTES", "120")),
|
|
per_run_limit=int(os.getenv("BLUEBUBBLES_CATCHUP_PER_RUN_LIMIT", "50")),
|
|
first_run_lookback_minutes=int(os.getenv("BLUEBUBBLES_CATCHUP_FIRST_LOOKBACK", "30")),
|
|
max_failure_retries=int(os.getenv("BLUEBUBBLES_CATCHUP_MAX_FAILURES", "10")),
|
|
),
|
|
health_monitor=HealthMonitorConfig(
|
|
enabled=os.getenv("BLUEBUBBLES_HEALTH_MONITOR_ENABLED", "true").lower() == "true",
|
|
),
|
|
)
|