Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用
## ✨ 近期更新

1. AstrBot 现已支持接入 [MCP](https://modelcontextprotocol.io/) 服务器!
2. 现已集成 [Google Agent SDK](https://google.github.io/adk-docs/)


## ✨ 主要功能

Expand Down Expand Up @@ -119,6 +121,7 @@ uvx astrbot init
| 微信客服 | ✔ | 私聊 | 文字、图片 |
| 飞书 | ✔ | 私聊、群聊 | 文字、图片 |
| 钉钉 | ✔ | 私聊、群聊 | 文字、图片 |
| Google Chat | ✔ | 私聊、群聊 | 文字、图片 |
| 微信对话开放平台 | 🚧 | 计划内 | - |
| Discord | 🚧 | 计划内 | - |
| WhatsApp | 🚧 | 计划内 | - |
Expand All @@ -131,6 +134,7 @@ uvx astrbot init
| OpenAI API | ✔ | 文本生成 | 也支持 DeepSeek、Google Gemini、GLM、Kimi、xAI 等兼容 OpenAI API 的服务 |
| Claude API | ✔ | 文本生成 | |
| Google Gemini API | ✔ | 文本生成 | |
| Google Agent SDK | ✔ | Agent SDK | [https://google.github.io/adk-docs/](https://google.github.io/adk-docs/) |
| Dify | ✔ | LLMOps | |
| 阿里云百炼应用 | ✔ | LLMOps | |
| Ollama | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
Expand Down
2 changes: 2 additions & 0 deletions README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ See docs: [Source Code Deployment](https://astrbot.app/deploy/astrbot/cli.html)
| [Telegram](https://github.com/Soulter/astrbot_plugin_telegram) | ✔ | Private/Group chats | Text, Images |
| [WeChat Work](https://github.com/Soulter/astrbot_plugin_wecom) | ✔ | Private chats | Text, Images, Voice |
| Feishu | ✔ | Group chats | Text, Images |
| Google Chat | ✔ | Private/Group chats | Text, Images |
| WeChat Open Platform | 🚧 | Planned | - |
| Discord | 🚧 | Planned | - |
| WhatsApp | 🚧 | Planned | - |
Expand All @@ -82,6 +83,7 @@ See docs: [Source Code Deployment](https://astrbot.app/deploy/astrbot/cli.html)
| OpenAI API | ✔ | Text Generation | Supports all OpenAI API-compatible services including DeepSeek, Google Gemini, GLM, Moonshot, Alibaba Cloud Bailian, Silicon Flow, xAI, etc. |
| Claude API | ✔ | Text Generation | |
| Google Gemini API | ✔ | Text Generation | |
| Google Agent SDK | ✔ | Agent SDK | [Documentation](https://google.github.io/adk-docs/) |
| Dify | ✔ | LLMOps | |
| DashScope (Alibaba Cloud) | ✔ | LLMOps | |
| Ollama | ✔ | Model Loader | Local deployment for open-source LLMs (DeepSeek, Llama, etc.) |
Expand Down
46 changes: 46 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@
"telegram_command_auto_refresh": True,
"telegram_command_register_interval": 300,
},
"google_chat": {
"id": "google_chat",
"type": "google_chat",
"enable": False,
"webhook_url": "",
"verification_token": "",
"callback_server_host": "0.0.0.0",
"port": 6200,
"bot_name": "astrbot",
},
},
"items": {
"active_send_mode": {
Expand Down Expand Up @@ -320,6 +330,31 @@
"hint": "请务必填对,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。",
"obvious_hint": True,
},
"google_chat_webhook_url": {
"description": "Google Chat Webhook URL",
"type": "string",
"hint": "发送消息到 Google Chat 的 Webhook 地址",
},
"google_chat_verification_token": {
"description": "Google Chat 验证 Token",
"type": "string",
"hint": "可选,用于验证来自 Google Chat 的回调请求",
},
"google_chat_callback_server_host": {
"description": "Google Chat 回调服务器 Host",
"type": "string",
"hint": "监听回调的服务器地址",
},
"google_chat_port": {
"description": "Google Chat 回调服务器端口",
"type": "int",
"hint": "监听回调的服务器端口",
},
"google_chat_bot_name": {
"description": "Google Chat 机器人名称",
"type": "string",
"hint": "机器人在 Google Chat 中显示的名称",
},
},
},
"platform_settings": {
Expand Down Expand Up @@ -630,6 +665,17 @@
"budget": 0,
},
},
"Google Agent SDK": {
"id": "google_agent_default",
"type": "google_agent_sdk",
"provider_type": "chat_completion",
"enable": False,
"key": [],
"timeout": 120,
"model_config": {
"model": "gemini-1.5-pro",
},
},
"DeepSeek": {
"id": "deepseek_default",
"type": "openai_chat_completion",
Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/platform/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ async def load_platform(self, platform_config: dict):
)
case "telegram":
from .sources.telegram.tg_adapter import TelegramPlatformAdapter # noqa: F401
case "google_chat":
from .sources.google_chat.google_chat_adapter import (
GoogleChatPlatformAdapter, # noqa: F401
)
case "wecom":
from .sources.wecom.wecom_adapter import WecomPlatformAdapter # noqa: F401
case "weixin_official_account":
Expand Down
Empty file.
133 changes: 133 additions & 0 deletions astrbot/core/platform/sources/google_chat/google_chat_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import asyncio
import uuid
import quart
import astrbot.api.message_components as Comp
from astrbot.api.platform import (
Platform,
AstrBotMessage,
MessageMember,
MessageType,
PlatformMetadata,
)
from astrbot.api.event import MessageChain
from astrbot.core.platform.astr_message_event import MessageSesion
from .google_chat_event import GoogleChatMessageEvent
from ...register import register_platform_adapter
from astrbot import logger


class GoogleChatServer:
def __init__(self, adapter: "GoogleChatPlatformAdapter", config: dict):
self.adapter = adapter
self.port = int(config.get("port", 6200))
self.host = config.get("callback_server_host", "0.0.0.0")
self.verification_token = config.get("verification_token", "")
self.server = quart.Quart(__name__)
self.server.add_url_rule(
"/astrbot-googlechat/callback", view_func=self.callback, methods=["POST"]
)
self.shutdown_event = asyncio.Event()

async def callback(self):
data = await quart.request.get_json()
token = quart.request.headers.get("Authorization")
if self.verification_token and token != f"Bearer {self.verification_token}":
logger.warning("Google Chat verification failed")
return {"success": False}, 403
await self.adapter.on_event(data)
return {"success": True}

async def start_polling(self):
logger.info(
f"Google Chat adapter listening on {self.host}:{self.port}"
)
await self.server.run_task(
host=self.host, port=self.port, shutdown_trigger=self.shutdown_trigger
)

async def shutdown_trigger(self):
await self.shutdown_event.wait()

async def shutdown(self):
self.shutdown_event.set()


@register_platform_adapter("google_chat", "Google Chat 适配器")
class GoogleChatPlatformAdapter(Platform):
def __init__(
self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue
) -> None:
super().__init__(event_queue)
self.config = platform_config
self.settings = platform_settings
self.bot_name = platform_config.get("bot_name", "astrbot")
self.unique_session = platform_settings["unique_session"]
self.server = GoogleChatServer(self, platform_config)

async def send_by_session(self, session: MessageSesion, message_chain: MessageChain):
await GoogleChatMessageEvent._send_chain(session.session_id, message_chain)
await super().send_by_session(session, message_chain)

def meta(self) -> PlatformMetadata:
return PlatformMetadata(
name="google_chat", description="Google Chat 适配器", id=self.config.get("id")
)

async def on_event(self, payload: dict):
abm = await self.convert_message(payload)
if abm:
await self.handle_msg(abm)

async def convert_message(self, payload: dict) -> AstrBotMessage | None:
if payload.get("type") != "MESSAGE":
return None
message = payload.get("message", {})
sender = message.get("sender", {})
text = message.get("text", "")
attachments = message.get("attachments", [])

abm = AstrBotMessage()
abm.message_id = message.get("name", str(uuid.uuid4()))
abm.sender = MessageMember(
user_id=sender.get("name", ""), nickname=sender.get("displayName", "")
)
abm.self_id = self.bot_name
abm.message_str = text
abm.message = [Comp.Plain(text)] if text else []
for att in attachments:
ctype = att.get("contentType", "")
if isinstance(ctype, str) and ctype.startswith("image/"):
url = att.get("downloadUri") or att.get("imageUri") or att.get("thumbnailUri")
if url:
abm.message.append(Comp.Image(file=url, url=url))
abm.message_str += " [图片]"

space = payload.get("space", {})
if space.get("type") == "ROOM":
abm.type = MessageType.GROUP_MESSAGE
abm.group_id = space.get("name")
else:
abm.type = MessageType.FRIEND_MESSAGE
abm.session_id = payload.get("responseUrl", self.config.get("webhook_url", ""))
abm.raw_message = payload
abm.timestamp = int(payload.get("eventTime", 0)) if isinstance(payload.get("eventTime"), int) else 0
return abm

async def handle_msg(self, abm: AstrBotMessage):
event = GoogleChatMessageEvent(
message_str=abm.message_str,
message_obj=abm,
platform_meta=self.meta(),
session_id=abm.session_id,
)
self.commit_event(event)

async def run(self):
await self.server.start_polling()

async def terminate(self):
await self.server.shutdown()
logger.info("Google Chat adapter shutdown")

def get_client(self):
return self.server
48 changes: 48 additions & 0 deletions astrbot/core/platform/sources/google_chat/google_chat_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import aiohttp
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.message_components import Plain, Image
from astrbot import logger


class GoogleChatMessageEvent(AstrMessageEvent):
@staticmethod
async def _send_chain(webhook_url: str, message: MessageChain):
text = ""
images = []
for comp in message.chain:
if isinstance(comp, Plain):
text += comp.text
elif isinstance(comp, Image):
if comp.file and comp.file.startswith("http"):
images.append(comp.file)
elif comp.url and comp.url.startswith("http"):
images.append(comp.url)
else:
try:
image_url = await comp.register_to_file_service()
images.append(image_url)
except Exception as e:
logger.error(f"Failed to register image: {e}")
payload = {}
if text:
payload["text"] = text
if images:
widgets = [{"image": {"imageUrl": url}} for url in images]
payload.setdefault("cards", []).append({"sections": [{"widgets": widgets}]})
if not payload:
payload["text"] = ""

async with aiohttp.ClientSession() as session:
async with session.post(webhook_url, json=payload) as resp:
if resp.status != 200:
try:
error_text = await resp.text()
except Exception:
error_text = resp.status
logger.error(
f"Failed to send Google Chat message: {error_text}"
)

async def send(self, message: MessageChain):
await self._send_chain(self.session_id, message)
await super().send(message)
4 changes: 4 additions & 0 deletions astrbot/core/provider/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ async def load_provider(self, provider_config: dict):
from .sources.gemini_source import (
ProviderGoogleGenAI as ProviderGoogleGenAI,
)
case "google_agent_sdk":
from .sources.google_agent_sdk_source import (
ProviderGoogleAgentSDK as ProviderGoogleAgentSDK,
)
case "sensevoice_stt_selfhost":
from .sources.sensevoice_selfhosted_source import (
ProviderSenseVoiceSTTSelfHost as ProviderSenseVoiceSTTSelfHost,
Expand Down
Loading