Merge branch 'feature/mcp-support' of github.com:SergioSV96/nanobot into feature/mcp-support
This commit is contained in:
commit
7052387f07
@ -50,6 +50,10 @@ class CronTool(Tool):
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Cron expression like '0 9 * * *' (for scheduled tasks)"
|
"description": "Cron expression like '0 9 * * *' (for scheduled tasks)"
|
||||||
},
|
},
|
||||||
|
"at": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ISO datetime for one-time execution (e.g. '2026-02-12T10:30:00')"
|
||||||
|
},
|
||||||
"job_id": {
|
"job_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Job ID (for remove)"
|
"description": "Job ID (for remove)"
|
||||||
@ -64,30 +68,38 @@ class CronTool(Tool):
|
|||||||
message: str = "",
|
message: str = "",
|
||||||
every_seconds: int | None = None,
|
every_seconds: int | None = None,
|
||||||
cron_expr: str | None = None,
|
cron_expr: str | None = None,
|
||||||
|
at: str | None = None,
|
||||||
job_id: str | None = None,
|
job_id: str | None = None,
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
) -> str:
|
) -> str:
|
||||||
if action == "add":
|
if action == "add":
|
||||||
return self._add_job(message, every_seconds, cron_expr)
|
return self._add_job(message, every_seconds, cron_expr, at)
|
||||||
elif action == "list":
|
elif action == "list":
|
||||||
return self._list_jobs()
|
return self._list_jobs()
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
return self._remove_job(job_id)
|
return self._remove_job(job_id)
|
||||||
return f"Unknown action: {action}"
|
return f"Unknown action: {action}"
|
||||||
|
|
||||||
def _add_job(self, message: str, every_seconds: int | None, cron_expr: str | None) -> str:
|
def _add_job(self, message: str, every_seconds: int | None, cron_expr: str | None, at: str | None) -> str:
|
||||||
if not message:
|
if not message:
|
||||||
return "Error: message is required for add"
|
return "Error: message is required for add"
|
||||||
if not self._channel or not self._chat_id:
|
if not self._channel or not self._chat_id:
|
||||||
return "Error: no session context (channel/chat_id)"
|
return "Error: no session context (channel/chat_id)"
|
||||||
|
|
||||||
# Build schedule
|
# Build schedule
|
||||||
|
delete_after = False
|
||||||
if every_seconds:
|
if every_seconds:
|
||||||
schedule = CronSchedule(kind="every", every_ms=every_seconds * 1000)
|
schedule = CronSchedule(kind="every", every_ms=every_seconds * 1000)
|
||||||
elif cron_expr:
|
elif cron_expr:
|
||||||
schedule = CronSchedule(kind="cron", expr=cron_expr)
|
schedule = CronSchedule(kind="cron", expr=cron_expr)
|
||||||
|
elif at:
|
||||||
|
from datetime import datetime
|
||||||
|
dt = datetime.fromisoformat(at)
|
||||||
|
at_ms = int(dt.timestamp() * 1000)
|
||||||
|
schedule = CronSchedule(kind="at", at_ms=at_ms)
|
||||||
|
delete_after = True
|
||||||
else:
|
else:
|
||||||
return "Error: either every_seconds or cron_expr is required"
|
return "Error: either every_seconds, cron_expr, or at is required"
|
||||||
|
|
||||||
job = self._cron.add_job(
|
job = self._cron.add_job(
|
||||||
name=message[:30],
|
name=message[:30],
|
||||||
@ -96,6 +108,7 @@ class CronTool(Tool):
|
|||||||
deliver=True,
|
deliver=True,
|
||||||
channel=self._channel,
|
channel=self._channel,
|
||||||
to=self._chat_id,
|
to=self._chat_id,
|
||||||
|
delete_after_run=delete_after,
|
||||||
)
|
)
|
||||||
return f"Created job '{job.name}' (id: {job.id})"
|
return f"Created job '{job.name}' (id: {job.id})"
|
||||||
|
|
||||||
|
|||||||
@ -137,8 +137,15 @@ class DingTalkChannel(BaseChannel):
|
|||||||
|
|
||||||
logger.info("DingTalk bot started with Stream Mode")
|
logger.info("DingTalk bot started with Stream Mode")
|
||||||
|
|
||||||
# client.start() is an async infinite loop handling the websocket connection
|
# Reconnect loop: restart stream if SDK exits or crashes
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
await self._client.start()
|
await self._client.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"DingTalk stream error: {e}")
|
||||||
|
if self._running:
|
||||||
|
logger.info("Reconnecting DingTalk stream in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Failed to start DingTalk channel: {e}")
|
logger.exception(f"Failed to start DingTalk channel: {e}")
|
||||||
|
|||||||
@ -98,12 +98,15 @@ class FeishuChannel(BaseChannel):
|
|||||||
log_level=lark.LogLevel.INFO
|
log_level=lark.LogLevel.INFO
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start WebSocket client in a separate thread
|
# Start WebSocket client in a separate thread with reconnect loop
|
||||||
def run_ws():
|
def run_ws():
|
||||||
|
while self._running:
|
||||||
try:
|
try:
|
||||||
self._ws_client.start()
|
self._ws_client.start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Feishu WebSocket error: {e}")
|
logger.warning(f"Feishu WebSocket error: {e}")
|
||||||
|
if self._running:
|
||||||
|
import time; time.sleep(5)
|
||||||
|
|
||||||
self._ws_thread = threading.Thread(target=run_ws, daemon=True)
|
self._ws_thread = threading.Thread(target=run_ws, daemon=True)
|
||||||
self._ws_thread.start()
|
self._ws_thread.start()
|
||||||
|
|||||||
@ -75,12 +75,15 @@ class QQChannel(BaseChannel):
|
|||||||
logger.info("QQ bot started (C2C private message)")
|
logger.info("QQ bot started (C2C private message)")
|
||||||
|
|
||||||
async def _run_bot(self) -> None:
|
async def _run_bot(self) -> None:
|
||||||
"""Run the bot connection."""
|
"""Run the bot connection with auto-reconnect."""
|
||||||
|
while self._running:
|
||||||
try:
|
try:
|
||||||
await self._client.start(appid=self.config.app_id, secret=self.config.secret)
|
await self._client.start(appid=self.config.app_id, secret=self.config.secret)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"QQ auth failed, check AppID/Secret at q.qq.com: {e}")
|
logger.warning(f"QQ bot error: {e}")
|
||||||
self._running = False
|
if self._running:
|
||||||
|
logger.info("Reconnecting QQ bot in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop the QQ bot."""
|
"""Stop the QQ bot."""
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from telegram import BotCommand, Update
|
from telegram import BotCommand, Update
|
||||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
||||||
|
from telegram.request import HTTPXRequest
|
||||||
|
|
||||||
from nanobot.bus.events import OutboundMessage
|
from nanobot.bus.events import OutboundMessage
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
@ -121,11 +122,13 @@ class TelegramChannel(BaseChannel):
|
|||||||
|
|
||||||
self._running = True
|
self._running = True
|
||||||
|
|
||||||
# Build the application
|
# Build the application with larger connection pool to avoid pool-timeout on long runs
|
||||||
builder = Application.builder().token(self.config.token)
|
req = HTTPXRequest(connection_pool_size=16, pool_timeout=5.0, connect_timeout=30.0, read_timeout=30.0)
|
||||||
|
builder = Application.builder().token(self.config.token).request(req).get_updates_request(req)
|
||||||
if self.config.proxy:
|
if self.config.proxy:
|
||||||
builder = builder.proxy(self.config.proxy).get_updates_proxy(self.config.proxy)
|
builder = builder.proxy(self.config.proxy).get_updates_proxy(self.config.proxy)
|
||||||
self._app = builder.build()
|
self._app = builder.build()
|
||||||
|
self._app.add_error_handler(self._on_error)
|
||||||
|
|
||||||
# Add command handlers
|
# Add command handlers
|
||||||
self._app.add_handler(CommandHandler("start", self._on_start))
|
self._app.add_handler(CommandHandler("start", self._on_start))
|
||||||
@ -386,6 +389,10 @@ class TelegramChannel(BaseChannel):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Typing indicator stopped for {chat_id}: {e}")
|
logger.debug(f"Typing indicator stopped for {chat_id}: {e}")
|
||||||
|
|
||||||
|
async def _on_error(self, update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Log polling / handler errors instead of silently swallowing them."""
|
||||||
|
logger.error(f"Telegram error: {context.error}")
|
||||||
|
|
||||||
def _get_extension(self, media_type: str, mime_type: str | None) -> str:
|
def _get_extension(self, media_type: str, mime_type: str | None) -> str:
|
||||||
"""Get file extension based on media type."""
|
"""Get file extension based on media type."""
|
||||||
if mime_type:
|
if mime_type:
|
||||||
|
|||||||
@ -7,10 +7,11 @@ description: Schedule reminders and recurring tasks.
|
|||||||
|
|
||||||
Use the `cron` tool to schedule reminders or recurring tasks.
|
Use the `cron` tool to schedule reminders or recurring tasks.
|
||||||
|
|
||||||
## Two Modes
|
## Three Modes
|
||||||
|
|
||||||
1. **Reminder** - message is sent directly to user
|
1. **Reminder** - message is sent directly to user
|
||||||
2. **Task** - message is a task description, agent executes and sends result
|
2. **Task** - message is a task description, agent executes and sends result
|
||||||
|
3. **One-time** - runs once at a specific time, then auto-deletes
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@ -24,6 +25,11 @@ Dynamic task (agent executes each time):
|
|||||||
cron(action="add", message="Check HKUDS/nanobot GitHub stars and report", every_seconds=600)
|
cron(action="add", message="Check HKUDS/nanobot GitHub stars and report", every_seconds=600)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
One-time scheduled task (compute ISO datetime from current time):
|
||||||
|
```
|
||||||
|
cron(action="add", message="Remind me about the meeting", at="<ISO datetime>")
|
||||||
|
```
|
||||||
|
|
||||||
List/remove:
|
List/remove:
|
||||||
```
|
```
|
||||||
cron(action="list")
|
cron(action="list")
|
||||||
@ -38,3 +44,4 @@ cron(action="remove", job_id="abc123")
|
|||||||
| every hour | every_seconds: 3600 |
|
| every hour | every_seconds: 3600 |
|
||||||
| every day at 8am | cron_expr: "0 8 * * *" |
|
| every day at 8am | cron_expr: "0 8 * * *" |
|
||||||
| weekdays at 5pm | cron_expr: "0 17 * * 1-5" |
|
| weekdays at 5pm | cron_expr: "0 17 * * 1-5" |
|
||||||
|
| at a specific time | at: ISO datetime string (compute from current time) |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user