resolve merge conflicts: keep both dingtalk and email channels

This commit is contained in:
Re-bin
2026-02-09 06:02:36 +00:00
16 changed files with 1278 additions and 233 deletions

View File

@@ -30,6 +30,14 @@ class FeishuConfig(BaseModel):
allow_from: list[str] = Field(default_factory=list) # Allowed user open_ids
class DingTalkConfig(BaseModel):
"""DingTalk channel configuration using Stream mode."""
enabled: bool = False
client_id: str = "" # AppKey
client_secret: str = "" # AppSecret
allow_from: list[str] = Field(default_factory=list) # Allowed staff_ids
class DiscordConfig(BaseModel):
"""Discord channel configuration."""
enabled: bool = False
@@ -75,6 +83,7 @@ class ChannelsConfig(BaseModel):
telegram: TelegramConfig = Field(default_factory=TelegramConfig)
discord: DiscordConfig = Field(default_factory=DiscordConfig)
feishu: FeishuConfig = Field(default_factory=FeishuConfig)
dingtalk: DingTalkConfig = Field(default_factory=DingTalkConfig)
email: EmailConfig = Field(default_factory=EmailConfig)
@@ -96,6 +105,7 @@ class ProviderConfig(BaseModel):
"""LLM provider configuration."""
api_key: str = ""
api_base: str | None = None
extra_headers: dict[str, str] | None = None # Custom headers (e.g. APP-Code for AiHubMix)
class ProvidersConfig(BaseModel):
@@ -110,6 +120,7 @@ class ProvidersConfig(BaseModel):
vllm: ProviderConfig = Field(default_factory=ProviderConfig)
gemini: ProviderConfig = Field(default_factory=ProviderConfig)
moonshot: ProviderConfig = Field(default_factory=ProviderConfig)
aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway
class GatewayConfig(BaseModel):
@@ -154,60 +165,52 @@ class Config(BaseSettings):
"""Get expanded workspace path."""
return Path(self.agents.defaults.workspace).expanduser()
def _match_provider(self, model: str | None = None) -> ProviderConfig | None:
"""Match a provider based on model name."""
model = (model or self.agents.defaults.model).lower()
# Map of keywords to provider configs
providers = {
"openrouter": self.providers.openrouter,
"deepseek": self.providers.deepseek,
"anthropic": self.providers.anthropic,
"claude": self.providers.anthropic,
"openai": self.providers.openai,
"gpt": self.providers.openai,
"gemini": self.providers.gemini,
"zhipu": self.providers.zhipu,
"glm": self.providers.zhipu,
"zai": self.providers.zhipu,
"dashscope": self.providers.dashscope,
"qwen": self.providers.dashscope,
"groq": self.providers.groq,
"moonshot": self.providers.moonshot,
"kimi": self.providers.moonshot,
"vllm": self.providers.vllm,
}
for keyword, provider in providers.items():
if keyword in model and provider.api_key:
return provider
return None
def _match_provider(self, model: str | None = None) -> tuple["ProviderConfig | None", str | None]:
"""Match provider config and its registry name. Returns (config, spec_name)."""
from nanobot.providers.registry import PROVIDERS
model_lower = (model or self.agents.defaults.model).lower()
# Match by keyword (order follows PROVIDERS registry)
for spec in PROVIDERS:
p = getattr(self.providers, spec.name, None)
if p and any(kw in model_lower for kw in spec.keywords) and p.api_key:
return p, spec.name
# Fallback: gateways first, then others (follows registry order)
for spec in PROVIDERS:
p = getattr(self.providers, spec.name, None)
if p and p.api_key:
return p, spec.name
return None, None
def get_provider(self, model: str | None = None) -> ProviderConfig | None:
"""Get matched provider config (api_key, api_base, extra_headers). Falls back to first available."""
p, _ = self._match_provider(model)
return p
def get_provider_name(self, model: str | None = None) -> str | None:
"""Get the registry name of the matched provider (e.g. "deepseek", "openrouter")."""
_, name = self._match_provider(model)
return name
def get_api_key(self, model: str | None = None) -> str | None:
"""Get API key for the given model (or default model). Falls back to first available key."""
# Try matching by model name first
matched = self._match_provider(model)
if matched:
return matched.api_key
# Fallback: return first available key
for provider in [
self.providers.openrouter, self.providers.deepseek,
self.providers.anthropic, self.providers.openai,
self.providers.gemini, self.providers.zhipu,
self.providers.dashscope, self.providers.moonshot,
self.providers.vllm, self.providers.groq,
]:
if provider.api_key:
return provider.api_key
return None
"""Get API key for the given model. Falls back to first available key."""
p = self.get_provider(model)
return p.api_key if p else None
def get_api_base(self, model: str | None = None) -> str | None:
"""Get API base URL based on model name."""
model = (model or self.agents.defaults.model).lower()
if "openrouter" in model:
return self.providers.openrouter.api_base or "https://openrouter.ai/api/v1"
if any(k in model for k in ("zhipu", "glm", "zai")):
return self.providers.zhipu.api_base
if "vllm" in model:
return self.providers.vllm.api_base
"""Get API base URL for the given model. Applies default URLs for known gateways."""
from nanobot.providers.registry import find_by_name
p, name = self._match_provider(model)
if p and p.api_base:
return p.api_base
# Only gateways get a default api_base here. Standard providers
# (like Moonshot) set their base URL via env vars in _setup_env
# to avoid polluting the global litellm.api_base.
if name:
spec = find_by_name(name)
if spec and spec.is_gateway and spec.default_api_base:
return spec.default_api_base
return None
class Config: