Merge branch 'main' into main
This commit is contained in:
commit
4d225ed2d6
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,4 +13,4 @@ docs/
|
|||||||
*.pyz
|
*.pyz
|
||||||
*.pywz
|
*.pywz
|
||||||
*.pyzz
|
*.pyzz
|
||||||
poetry.lock
|
poetry.lock
|
||||||
|
|||||||
63
README.md
63
README.md
@ -171,12 +171,13 @@ nanobot agent -m "Hello from my local LLM!"
|
|||||||
|
|
||||||
## 💬 Chat Apps
|
## 💬 Chat Apps
|
||||||
|
|
||||||
Talk to your nanobot through Telegram or WhatsApp — anytime, anywhere.
|
Talk to your nanobot through Telegram, WhatsApp, or Feishu — anytime, anywhere.
|
||||||
|
|
||||||
| Channel | Setup |
|
| Channel | Setup |
|
||||||
|---------|-------|
|
|---------|-------|
|
||||||
| **Telegram** | Easy (just a token) |
|
| **Telegram** | Easy (just a token) |
|
||||||
| **WhatsApp** | Medium (scan QR) |
|
| **WhatsApp** | Medium (scan QR) |
|
||||||
|
| **Feishu** | Medium (app credentials) |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Telegram</b> (Recommended)</summary>
|
<summary><b>Telegram</b> (Recommended)</summary>
|
||||||
@ -247,6 +248,55 @@ nanobot gateway
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Feishu (飞书)</b></summary>
|
||||||
|
|
||||||
|
Uses **WebSocket** long connection — no public IP required.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install nanobot-ai[feishu]
|
||||||
|
```
|
||||||
|
|
||||||
|
**1. Create a Feishu bot**
|
||||||
|
- Visit [Feishu Open Platform](https://open.feishu.cn/app)
|
||||||
|
- Create a new app → Enable **Bot** capability
|
||||||
|
- **Permissions**: Add `im:message` (send messages)
|
||||||
|
- **Events**: Add `im.message.receive_v1` (receive messages)
|
||||||
|
- Select **Long Connection** mode (requires running nanobot first to establish connection)
|
||||||
|
- Get **App ID** and **App Secret** from "Credentials & Basic Info"
|
||||||
|
- Publish the app
|
||||||
|
|
||||||
|
**2. Configure**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"feishu": {
|
||||||
|
"enabled": true,
|
||||||
|
"appId": "cli_xxx",
|
||||||
|
"appSecret": "xxx",
|
||||||
|
"encryptKey": "",
|
||||||
|
"verificationToken": "",
|
||||||
|
"allowFrom": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `encryptKey` and `verificationToken` are optional for Long Connection mode.
|
||||||
|
> `allowFrom`: Leave empty to allow all users, or add `["ou_xxx"]` to restrict access.
|
||||||
|
|
||||||
|
**3. Run**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nanobot gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Feishu uses WebSocket to receive messages — no webhook or public IP needed!
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
Config file: `~/.nanobot/config.json`
|
Config file: `~/.nanobot/config.json`
|
||||||
@ -261,6 +311,7 @@ Config file: `~/.nanobot/config.json`
|
|||||||
| `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
|
| `openrouter` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
|
||||||
| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
|
| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
|
||||||
| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
|
| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
|
||||||
|
| `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
|
||||||
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
|
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
|
||||||
| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
|
| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
|
||||||
|
|
||||||
@ -291,6 +342,14 @@ Config file: `~/.nanobot/config.json`
|
|||||||
},
|
},
|
||||||
"whatsapp": {
|
"whatsapp": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
|
},
|
||||||
|
"feishu": {
|
||||||
|
"enabled": false,
|
||||||
|
"appId": "cli_xxx",
|
||||||
|
"appSecret": "xxx",
|
||||||
|
"encryptKey": "",
|
||||||
|
"verificationToken": "",
|
||||||
|
"allowFrom": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
@ -397,7 +456,7 @@ PRs welcome! The codebase is intentionally small and readable. 🤗
|
|||||||
### Contributors
|
### Contributors
|
||||||
|
|
||||||
<a href="https://github.com/HKUDS/nanobot/graphs/contributors">
|
<a href="https://github.com/HKUDS/nanobot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=HKUDS/nanobot" />
|
<img src="https://contrib.rocks/image?repo=HKUDS/nanobot&max=100&columns=12" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
263
nanobot/channels/feishu.py
Normal file
263
nanobot/channels/feishu.py
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
"""Feishu/Lark channel implementation using lark-oapi SDK with WebSocket long connection."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from nanobot.bus.events import OutboundMessage
|
||||||
|
from nanobot.bus.queue import MessageBus
|
||||||
|
from nanobot.channels.base import BaseChannel
|
||||||
|
from nanobot.config.schema import FeishuConfig
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lark_oapi as lark
|
||||||
|
from lark_oapi.api.im.v1 import (
|
||||||
|
CreateMessageRequest,
|
||||||
|
CreateMessageRequestBody,
|
||||||
|
CreateMessageReactionRequest,
|
||||||
|
CreateMessageReactionRequestBody,
|
||||||
|
Emoji,
|
||||||
|
P2ImMessageReceiveV1,
|
||||||
|
)
|
||||||
|
FEISHU_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
FEISHU_AVAILABLE = False
|
||||||
|
lark = None
|
||||||
|
Emoji = None
|
||||||
|
|
||||||
|
# Message type display mapping
|
||||||
|
MSG_TYPE_MAP = {
|
||||||
|
"image": "[image]",
|
||||||
|
"audio": "[audio]",
|
||||||
|
"file": "[file]",
|
||||||
|
"sticker": "[sticker]",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FeishuChannel(BaseChannel):
|
||||||
|
"""
|
||||||
|
Feishu/Lark channel using WebSocket long connection.
|
||||||
|
|
||||||
|
Uses WebSocket to receive events - no public IP or webhook required.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
- App ID and App Secret from Feishu Open Platform
|
||||||
|
- Bot capability enabled
|
||||||
|
- Event subscription enabled (im.message.receive_v1)
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "feishu"
|
||||||
|
|
||||||
|
def __init__(self, config: FeishuConfig, bus: MessageBus):
|
||||||
|
super().__init__(config, bus)
|
||||||
|
self.config: FeishuConfig = config
|
||||||
|
self._client: Any = None
|
||||||
|
self._ws_client: Any = None
|
||||||
|
self._ws_thread: threading.Thread | None = None
|
||||||
|
self._processed_message_ids: OrderedDict[str, None] = OrderedDict() # Ordered dedup cache
|
||||||
|
self._loop: asyncio.AbstractEventLoop | None = None
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""Start the Feishu bot with WebSocket long connection."""
|
||||||
|
if not FEISHU_AVAILABLE:
|
||||||
|
logger.error("Feishu SDK not installed. Run: pip install lark-oapi")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.config.app_id or not self.config.app_secret:
|
||||||
|
logger.error("Feishu app_id and app_secret not configured")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._running = True
|
||||||
|
self._loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
# Create Lark client for sending messages
|
||||||
|
self._client = lark.Client.builder() \
|
||||||
|
.app_id(self.config.app_id) \
|
||||||
|
.app_secret(self.config.app_secret) \
|
||||||
|
.log_level(lark.LogLevel.INFO) \
|
||||||
|
.build()
|
||||||
|
|
||||||
|
# Create event handler (only register message receive, ignore other events)
|
||||||
|
event_handler = lark.EventDispatcherHandler.builder(
|
||||||
|
self.config.encrypt_key or "",
|
||||||
|
self.config.verification_token or "",
|
||||||
|
).register_p2_im_message_receive_v1(
|
||||||
|
self._on_message_sync
|
||||||
|
).build()
|
||||||
|
|
||||||
|
# Create WebSocket client for long connection
|
||||||
|
self._ws_client = lark.ws.Client(
|
||||||
|
self.config.app_id,
|
||||||
|
self.config.app_secret,
|
||||||
|
event_handler=event_handler,
|
||||||
|
log_level=lark.LogLevel.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start WebSocket client in a separate thread
|
||||||
|
def run_ws():
|
||||||
|
try:
|
||||||
|
self._ws_client.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Feishu WebSocket error: {e}")
|
||||||
|
|
||||||
|
self._ws_thread = threading.Thread(target=run_ws, daemon=True)
|
||||||
|
self._ws_thread.start()
|
||||||
|
|
||||||
|
logger.info("Feishu bot started with WebSocket long connection")
|
||||||
|
logger.info("No public IP required - using WebSocket to receive events")
|
||||||
|
|
||||||
|
# Keep running until stopped
|
||||||
|
while self._running:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
"""Stop the Feishu bot."""
|
||||||
|
self._running = False
|
||||||
|
if self._ws_client:
|
||||||
|
try:
|
||||||
|
self._ws_client.stop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error stopping WebSocket client: {e}")
|
||||||
|
logger.info("Feishu bot stopped")
|
||||||
|
|
||||||
|
def _add_reaction_sync(self, message_id: str, emoji_type: str) -> None:
|
||||||
|
"""Sync helper for adding reaction (runs in thread pool)."""
|
||||||
|
try:
|
||||||
|
request = CreateMessageReactionRequest.builder() \
|
||||||
|
.message_id(message_id) \
|
||||||
|
.request_body(
|
||||||
|
CreateMessageReactionRequestBody.builder()
|
||||||
|
.reaction_type(Emoji.builder().emoji_type(emoji_type).build())
|
||||||
|
.build()
|
||||||
|
).build()
|
||||||
|
|
||||||
|
response = self._client.im.v1.message_reaction.create(request)
|
||||||
|
|
||||||
|
if not response.success():
|
||||||
|
logger.warning(f"Failed to add reaction: code={response.code}, msg={response.msg}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Added {emoji_type} reaction to message {message_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error adding reaction: {e}")
|
||||||
|
|
||||||
|
async def _add_reaction(self, message_id: str, emoji_type: str = "THUMBSUP") -> None:
|
||||||
|
"""
|
||||||
|
Add a reaction emoji to a message (non-blocking).
|
||||||
|
|
||||||
|
Common emoji types: THUMBSUP, OK, EYES, DONE, OnIt, HEART
|
||||||
|
"""
|
||||||
|
if not self._client or not Emoji:
|
||||||
|
return
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
await loop.run_in_executor(None, self._add_reaction_sync, message_id, emoji_type)
|
||||||
|
|
||||||
|
async def send(self, msg: OutboundMessage) -> None:
|
||||||
|
"""Send a message through Feishu."""
|
||||||
|
if not self._client:
|
||||||
|
logger.warning("Feishu client not initialized")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine receive_id_type based on chat_id format
|
||||||
|
# open_id starts with "ou_", chat_id starts with "oc_"
|
||||||
|
if msg.chat_id.startswith("oc_"):
|
||||||
|
receive_id_type = "chat_id"
|
||||||
|
else:
|
||||||
|
receive_id_type = "open_id"
|
||||||
|
|
||||||
|
# Build text message content
|
||||||
|
content = json.dumps({"text": msg.content})
|
||||||
|
|
||||||
|
request = CreateMessageRequest.builder() \
|
||||||
|
.receive_id_type(receive_id_type) \
|
||||||
|
.request_body(
|
||||||
|
CreateMessageRequestBody.builder()
|
||||||
|
.receive_id(msg.chat_id)
|
||||||
|
.msg_type("text")
|
||||||
|
.content(content)
|
||||||
|
.build()
|
||||||
|
).build()
|
||||||
|
|
||||||
|
response = self._client.im.v1.message.create(request)
|
||||||
|
|
||||||
|
if not response.success():
|
||||||
|
logger.error(
|
||||||
|
f"Failed to send Feishu message: code={response.code}, "
|
||||||
|
f"msg={response.msg}, log_id={response.get_log_id()}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Feishu message sent to {msg.chat_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending Feishu message: {e}")
|
||||||
|
|
||||||
|
def _on_message_sync(self, data: "P2ImMessageReceiveV1") -> None:
|
||||||
|
"""
|
||||||
|
Sync handler for incoming messages (called from WebSocket thread).
|
||||||
|
Schedules async handling in the main event loop.
|
||||||
|
"""
|
||||||
|
if self._loop and self._loop.is_running():
|
||||||
|
asyncio.run_coroutine_threadsafe(self._on_message(data), self._loop)
|
||||||
|
|
||||||
|
async def _on_message(self, data: "P2ImMessageReceiveV1") -> None:
|
||||||
|
"""Handle incoming message from Feishu."""
|
||||||
|
try:
|
||||||
|
event = data.event
|
||||||
|
message = event.message
|
||||||
|
sender = event.sender
|
||||||
|
|
||||||
|
# Deduplication check
|
||||||
|
message_id = message.message_id
|
||||||
|
if message_id in self._processed_message_ids:
|
||||||
|
return
|
||||||
|
self._processed_message_ids[message_id] = None
|
||||||
|
|
||||||
|
# Trim cache: keep most recent 500 when exceeds 1000
|
||||||
|
while len(self._processed_message_ids) > 1000:
|
||||||
|
self._processed_message_ids.popitem(last=False)
|
||||||
|
|
||||||
|
# Skip bot messages
|
||||||
|
sender_type = sender.sender_type
|
||||||
|
if sender_type == "bot":
|
||||||
|
return
|
||||||
|
|
||||||
|
sender_id = sender.sender_id.open_id if sender.sender_id else "unknown"
|
||||||
|
chat_id = message.chat_id
|
||||||
|
chat_type = message.chat_type # "p2p" or "group"
|
||||||
|
msg_type = message.message_type
|
||||||
|
|
||||||
|
# Add reaction to indicate "seen"
|
||||||
|
await self._add_reaction(message_id, "THUMBSUP")
|
||||||
|
|
||||||
|
# Parse message content
|
||||||
|
if msg_type == "text":
|
||||||
|
try:
|
||||||
|
content = json.loads(message.content).get("text", "")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
content = message.content or ""
|
||||||
|
else:
|
||||||
|
content = MSG_TYPE_MAP.get(msg_type, f"[{msg_type}]")
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Forward to message bus
|
||||||
|
reply_to = chat_id if chat_type == "group" else sender_id
|
||||||
|
await self._handle_message(
|
||||||
|
sender_id=sender_id,
|
||||||
|
chat_id=reply_to,
|
||||||
|
content=content,
|
||||||
|
metadata={
|
||||||
|
"message_id": message_id,
|
||||||
|
"chat_type": chat_type,
|
||||||
|
"msg_type": msg_type,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing Feishu message: {e}")
|
||||||
@ -55,6 +55,17 @@ class ChannelManager:
|
|||||||
logger.info("WhatsApp channel enabled")
|
logger.info("WhatsApp channel enabled")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.warning(f"WhatsApp channel not available: {e}")
|
logger.warning(f"WhatsApp channel not available: {e}")
|
||||||
|
|
||||||
|
# Feishu channel
|
||||||
|
if self.config.channels.feishu.enabled:
|
||||||
|
try:
|
||||||
|
from nanobot.channels.feishu import FeishuChannel
|
||||||
|
self.channels["feishu"] = FeishuChannel(
|
||||||
|
self.config.channels.feishu, self.bus
|
||||||
|
)
|
||||||
|
logger.info("Feishu channel enabled")
|
||||||
|
except ImportError as e:
|
||||||
|
logger.warning(f"Feishu channel not available: {e}")
|
||||||
|
|
||||||
async def start_all(self) -> None:
|
async def start_all(self) -> None:
|
||||||
"""Start WhatsApp channel and the outbound dispatcher."""
|
"""Start WhatsApp channel and the outbound dispatcher."""
|
||||||
|
|||||||
@ -17,12 +17,24 @@ class TelegramConfig(BaseModel):
|
|||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
token: str = "" # Bot token from @BotFather
|
token: str = "" # Bot token from @BotFather
|
||||||
allow_from: list[str] = Field(default_factory=list) # Allowed user IDs or usernames
|
allow_from: list[str] = Field(default_factory=list) # Allowed user IDs or usernames
|
||||||
|
proxy: str | None = None # HTTP/SOCKS5 proxy URL, e.g. "http://127.0.0.1:7890" or "socks5://127.0.0.1:1080"
|
||||||
|
|
||||||
|
|
||||||
|
class FeishuConfig(BaseModel):
|
||||||
|
"""Feishu/Lark channel configuration using WebSocket long connection."""
|
||||||
|
enabled: bool = False
|
||||||
|
app_id: str = "" # App ID from Feishu Open Platform
|
||||||
|
app_secret: str = "" # App Secret from Feishu Open Platform
|
||||||
|
encrypt_key: str = "" # Encrypt Key for event subscription (optional)
|
||||||
|
verification_token: str = "" # Verification Token for event subscription (optional)
|
||||||
|
allow_from: list[str] = Field(default_factory=list) # Allowed user open_ids
|
||||||
|
|
||||||
|
|
||||||
class ChannelsConfig(BaseModel):
|
class ChannelsConfig(BaseModel):
|
||||||
"""Configuration for chat channels."""
|
"""Configuration for chat channels."""
|
||||||
whatsapp: WhatsAppConfig = Field(default_factory=WhatsAppConfig)
|
whatsapp: WhatsAppConfig = Field(default_factory=WhatsAppConfig)
|
||||||
telegram: TelegramConfig = Field(default_factory=TelegramConfig)
|
telegram: TelegramConfig = Field(default_factory=TelegramConfig)
|
||||||
|
feishu: FeishuConfig = Field(default_factory=FeishuConfig)
|
||||||
|
|
||||||
|
|
||||||
class AgentDefaults(BaseModel):
|
class AgentDefaults(BaseModel):
|
||||||
@ -50,6 +62,7 @@ class ProvidersConfig(BaseModel):
|
|||||||
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
|
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
openai: ProviderConfig = Field(default_factory=ProviderConfig)
|
openai: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
|
openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
|
deepseek: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
groq: ProviderConfig = Field(default_factory=ProviderConfig)
|
groq: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
zhipu: ProviderConfig = Field(default_factory=ProviderConfig)
|
zhipu: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
vllm: ProviderConfig = Field(default_factory=ProviderConfig)
|
vllm: ProviderConfig = Field(default_factory=ProviderConfig)
|
||||||
@ -99,9 +112,10 @@ class Config(BaseSettings):
|
|||||||
return Path(self.agents.defaults.workspace).expanduser()
|
return Path(self.agents.defaults.workspace).expanduser()
|
||||||
|
|
||||||
def get_api_key(self) -> str | None:
|
def get_api_key(self) -> str | None:
|
||||||
"""Get API key in priority order: OpenRouter > Anthropic > OpenAI > Gemini > Zhipu > Groq > vLLM."""
|
"""Get API key in priority order: OpenRouter > DeepSeek > Anthropic > OpenAI > Gemini > Zhipu > Groq > vLLM."""
|
||||||
return (
|
return (
|
||||||
self.providers.openrouter.api_key or
|
self.providers.openrouter.api_key or
|
||||||
|
self.providers.deepseek.api_key or
|
||||||
self.providers.anthropic.api_key or
|
self.providers.anthropic.api_key or
|
||||||
self.providers.openai.api_key or
|
self.providers.openai.api_key or
|
||||||
self.providers.gemini.api_key or
|
self.providers.gemini.api_key or
|
||||||
|
|||||||
@ -43,6 +43,8 @@ class LiteLLMProvider(LLMProvider):
|
|||||||
elif self.is_vllm:
|
elif self.is_vllm:
|
||||||
# vLLM/custom endpoint - uses OpenAI-compatible API
|
# vLLM/custom endpoint - uses OpenAI-compatible API
|
||||||
os.environ["OPENAI_API_KEY"] = api_key
|
os.environ["OPENAI_API_KEY"] = api_key
|
||||||
|
elif "deepseek" in default_model:
|
||||||
|
os.environ.setdefault("DEEPSEEK_API_KEY", api_key)
|
||||||
elif "anthropic" in default_model:
|
elif "anthropic" in default_model:
|
||||||
os.environ.setdefault("ANTHROPIC_API_KEY", api_key)
|
os.environ.setdefault("ANTHROPIC_API_KEY", api_key)
|
||||||
elif "openai" in default_model or "gpt" in default_model:
|
elif "openai" in default_model or "gpt" in default_model:
|
||||||
|
|||||||
@ -32,6 +32,9 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
feishu = [
|
||||||
|
"lark-oapi>=1.0.0",
|
||||||
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=7.0.0",
|
"pytest>=7.0.0",
|
||||||
"pytest-asyncio>=0.21.0",
|
"pytest-asyncio>=0.21.0",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user