Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
5029d89630 Address code review feedback
- Add validation to ensure only one context parameter is provided in get_message_by_id
- Simplify logger check in error handling

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-12-03 14:16:16 +00:00
copilot-swe-agent[bot]
b081ef89d5 Add QQ bot quoted message support for Dify integration
- Added message_reference property to QQOfficialEvent class
- Implemented get_message_by_id method in QQOfficialClient to fetch referenced messages
- Updated QQOfficialMessageConverter to handle quoted messages and create Quote components
- Modified QQOfficialEventConverter to pass message reference context
- Updated QQOfficialAdapter to properly initialize converters with bot reference
- Added quoted_message_text variable in pipeline preprocessor for Dify workflow integration

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-12-03 14:13:47 +00:00
copilot-swe-agent[bot]
75d449f21a Initial plan 2025-12-03 14:06:21 +00:00
8 changed files with 151 additions and 608 deletions

View File

@@ -1,162 +0,0 @@
# Microsoft Teams Adapter Implementation
## Overview
A new Microsoft Teams platform adapter has been added to LangBot, enabling support for both personal chats and channel/group chats on Microsoft Teams.
## Files Created/Modified
### New Files
1. **`src/langbot/pkg/platform/sources/teams.py`** - Main adapter implementation
- `TeamsMessageConverter`: Converts between LangBot message format and Teams Activity format
- `TeamsEventConverter`: Converts between Teams Activity events and LangBot platform events
- `TeamsAdapter`: Main adapter class with webhook handling
2. **`src/langbot/pkg/platform/sources/teams.yaml`** - Adapter manifest
- Defines adapter metadata, configuration schema, and execution details
- Configuration fields: `app_id` and `app_password`
### Modified Files
1. **`pyproject.toml`** - Added dependencies:
- `botbuilder-core>=4.15.0`
- `botbuilder-schema>=4.15.0`
- `botframework-connector>=4.15.0`
- Added "teams" keyword
## Features
### Supported Message Types
- ✅ Plain text messages
- ✅ Image attachments (base64, URL, and file path)
-@mentions (converted to At components)
- ✅ Message replies with quote support
### Supported Chat Types
- ✅ Personal chats (1-on-1 conversations)
- ✅ Channel chats (group/team conversations)
### Message Handling
- **Incoming Messages**: Received via webhook at the unified webhook endpoint
- **Outgoing Messages**: Sent via Bot Framework SDK using conversation references
- **Event Types**: FriendMessage (personal) and GroupMessage (channel/group)
## Configuration Requirements
### Azure Setup
1. **Register an Azure AD Application**:
- Go to Azure Portal → Azure Active Directory → App registrations
- Create a new registration
- Note the **Application (client) ID** - this is your `app_id`
- Create a **Client Secret** - this is your `app_password`
2. **Create Azure Bot Resource**:
- Go to Azure Portal → Create a resource → Azure Bot
- Link it to your Azure AD application
- Enable the Microsoft Teams channel
- Set the messaging endpoint to: `https://<your-domain>/bots/<bot-uuid>`
### LangBot Configuration
When creating a Teams bot in LangBot, provide:
```yaml
adapter: teams
adapter_config:
app_id: "<your-microsoft-app-id>"
app_password: "<your-microsoft-app-password>"
```
## Architecture
### Webhook Mode
The Teams adapter operates in webhook mode, similar to the Slack adapter:
- Integrates with LangBot's unified webhook system
- Receives messages at `/bots/<bot-uuid>` endpoint
- No independent server process required
### Message Flow
1. **Incoming**:
- Teams → Azure Bot Service → LangBot Webhook
- Bot Framework validates the request
- Activity converted to LangBot event format
- Event dispatched to registered listeners
2. **Outgoing**:
- LangBot message chain → Teams Activity format
- Reply sent via Bot Framework adapter
- Uses conversation reference for proper routing
## Authentication
- Uses Bot Framework authentication with Microsoft App credentials
- JWT token validation handled by `BotFrameworkAdapter`
- Authorization header validated on each incoming request
## Limitations & Notes
1. **Direct Send**: The `send_message` method is limited - use `reply_message` for best results
2. **Conversation References**: The adapter uses conversation references from incoming activities to send replies
3. **Image Handling**: Images are converted to inline attachments using data URIs
4. **Streaming**: Streaming replies are not yet implemented
## Testing
To test the adapter:
1. Install dependencies: `uv sync`
2. Verify adapter initialization: `uv run python -c "from src.langbot.pkg.platform.sources.teams import TeamsAdapter; print('OK')"`
3. Configure a Teams bot in LangBot with your Azure credentials
4. Expose the LangBot API endpoint publicly (use ngrok or similar)
5. Set the messaging endpoint in Azure Bot Service
6. Add the bot to Teams and start chatting
## Troubleshooting
### Pydantic Validation Error on Initialization
**Fixed**: The adapter was updated to properly handle optional fields (`adapter`, `app`, `bot_uuid`) by:
- Setting `default=None` in pydantic.Field definitions
- Not passing these fields to `super().__init__()`
- Setting the adapter instance after parent class initialization
This resolves the validation error: "Input should be an instance of Quart" / "Input should be a valid string"
## Future Enhancements
Potential improvements:
- Adaptive Cards support
- Rich message formatting (markdown, cards)
- File attachments (non-image)
- Streaming message support
- Proactive messaging
- Team/channel member list retrieval
- Reaction handling
## Dependencies
The following Python packages were added:
- `botbuilder-core` - Core Bot Framework functionality
- `botbuilder-schema` - Activity and entity schemas
- `botframework-connector` - Bot Framework connector for authentication
## References
- [Microsoft Teams Bot Framework Documentation](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/bot-features)
- [Bot Framework SDK for Python](https://github.com/microsoft/botbuilder-python)
- [Teams Conversation Bot Sample](https://learn.microsoft.com/en-us/samples/officedev/microsoft-teams-samples/officedev-microsoft-teams-samples-bot-conversation-python/)
## Sources
Research sources consulted during implementation:
- [Create an Incoming Webhook - Teams | Microsoft Learn](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook)
- [Teams Conversation Bot - Code Samples | Microsoft Learn](https://learn.microsoft.com/en-us/samples/officedev/microsoft-teams-samples/officedev-microsoft-teams-samples-bot-conversation-python/)
- [GitHub - microsoft/botbuilder-python](https://github.com/microsoft/botbuilder-python)
- [Tools and Bot Framework SDKs for Bots - Teams](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/bot-features)

View File

@@ -14,9 +14,6 @@ dependencies = [
"anthropic>=0.51.0",
"argon2-cffi>=23.1.0",
"async-lru>=2.0.5",
"botbuilder-core>=4.15.0",
"botbuilder-schema>=4.15.0",
"botframework-connector>=4.15.0",
"certifi>=2025.4.26",
"colorlog~=6.6.0",
"cryptography>=44.0.3",
@@ -76,7 +73,6 @@ keywords = [
"bot",
"agent",
"telegram",
"teams",
"plugins",
"openai",
"instant-messaging",

View File

@@ -166,6 +166,11 @@ class QQOfficialClient:
else:
message_data['image_attachments'] = None
# Extract message_reference if present
message_reference = msg.get('d', {}).get('message_reference', {})
if message_reference:
message_data['message_reference'] = message_reference
return message_data
async def is_image(self, attachment: dict) -> bool:
@@ -272,6 +277,57 @@ class QQOfficialClient:
return True
return time.time() > self.access_token_expiry_time
async def get_message_by_id(self, message_id: str, channel_id: str = None, group_openid: str = None, user_openid: str = None) -> Dict[str, Any]:
"""根据消息ID获取消息内容
Args:
message_id: 消息ID
channel_id: 频道ID频道消息需要
group_openid: 群组openid群消息需要
user_openid: 用户openid私聊消息需要
Returns:
消息内容字典
"""
if not await self.check_access_token():
await self.get_access_token()
# Validate that exactly one context parameter is provided
provided_contexts = sum([bool(channel_id), bool(group_openid), bool(user_openid)])
if provided_contexts == 0:
await self.logger.warning(f'Cannot fetch message {message_id}: no context provided')
return {}
if provided_contexts > 1:
await self.logger.warning(f'Cannot fetch message {message_id}: multiple contexts provided')
return {}
# Determine which API endpoint to use based on provided parameters
if channel_id:
# Channel message
url = f'{self.base_url}/channels/{channel_id}/messages/{message_id}'
elif group_openid:
# Group message
url = f'{self.base_url}/v2/groups/{group_openid}/messages/{message_id}'
elif user_openid:
# Private message
url = f'{self.base_url}/v2/users/{user_openid}/messages/{message_id}'
async with httpx.AsyncClient() as client:
headers = {
'Authorization': f'QQBot {self.access_token}',
'Content-Type': 'application/json',
}
try:
response = await client.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
await self.logger.warning(f'Failed to fetch message {message_id}: {response.status_code}')
return {}
except Exception as e:
await self.logger.warning(f'Error fetching message {message_id}: {e}')
return {}
async def repeat_seed(self, bot_secret: str, target_size: int = 32) -> bytes:
seed = bot_secret
while len(seed) < target_size:

View File

@@ -110,3 +110,10 @@ class QQOfficialEvent(dict):
文件类型
"""
return self.get('content_type', '')
@property
def message_reference(self) -> dict:
"""
引用消息
"""
return self.get('message_reference', {})

View File

@@ -100,6 +100,7 @@ class PreProcessor(stage.PipelineStage):
plain_text = ''
quote_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
quoted_text = '' # Store quoted message text
for me in query.message_chain:
if isinstance(me, platform_message.Plain):
@@ -117,6 +118,7 @@ class PreProcessor(stage.PipelineStage):
elif isinstance(me, platform_message.Quote) and quote_msg:
for msg in me.origin:
if isinstance(msg, platform_message.Plain):
quoted_text += msg.text
content_list.append(provider_message.ContentElement.from_text(msg.text))
elif isinstance(msg, platform_message.Image):
if selected_runner != 'local-agent' or (
@@ -126,6 +128,7 @@ class PreProcessor(stage.PipelineStage):
content_list.append(provider_message.ContentElement.from_image_base64(msg.base64))
query.variables['user_message_text'] = plain_text
query.variables['quoted_message_text'] = quoted_text # Add quoted text as variable
query.user_message = provider_message.Message(role='user', content=content_list)
# =========== 触发事件 PromptPreProcessing

View File

@@ -16,6 +16,9 @@ from ..logger import EventLogger
class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
def __init__(self, bot: QQOfficialClient = None):
self.bot = bot
@staticmethod
async def yiri2target(message_chain: platform_message.MessageChain):
content_list = []
@@ -31,10 +34,63 @@ class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConver
return content_list
@staticmethod
async def target2yiri(message: str, message_id: str, pic_url: str, content_type):
async def target2yiri(self, message: str, message_id: str, pic_url: str, content_type, message_reference: dict = None, event_type: str = None, channel_id: str = None, group_openid: str = None, user_openid: str = None):
yiri_msg_list = []
yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now()))
# Handle quoted message if message_reference exists
if message_reference and message_reference.get('message_id') and self.bot:
referenced_msg_id = message_reference.get('message_id')
try:
# Fetch the referenced message
referenced_msg = await self.bot.get_message_by_id(
referenced_msg_id,
channel_id=channel_id,
group_openid=group_openid,
user_openid=user_openid
)
if referenced_msg:
# Create message chain for the quoted content
quoted_content = referenced_msg.get('content', '')
quoted_chain = platform_message.MessageChain()
if quoted_content:
quoted_chain.append(platform_message.Plain(text=quoted_content))
# Add images if present in quoted message
quoted_attachments = referenced_msg.get('attachments', [])
for attachment in quoted_attachments:
if attachment.get('content_type', '').startswith('image/'):
img_url = attachment.get('url', '')
if img_url:
try:
img_base64 = await image.get_qq_official_image_base64(
pic_url=img_url if img_url.startswith('https://') else 'https://' + img_url,
content_type=attachment.get('content_type', '')
)
quoted_chain.append(platform_message.Image(base64=img_base64))
except Exception:
# If image fetch fails, just skip it
pass
# Get sender info from referenced message
quoted_sender_id = referenced_msg.get('author', {}).get('id', '') or \
referenced_msg.get('author', {}).get('user_openid', '') or \
referenced_msg.get('author', {}).get('member_openid', '')
# Add Quote component
yiri_msg_list.append(
platform_message.Quote(
id=referenced_msg_id,
sender_id=quoted_sender_id,
origin=quoted_chain,
)
)
except Exception as e:
# If fetching quoted message fails, log and continue
await self.bot.logger.warning(f'Failed to fetch quoted message {referenced_msg_id}: {e}')
if pic_url is not None:
base64_url = await image.get_qq_official_image_base64(pic_url=pic_url, content_type=content_type)
yiri_msg_list.append(platform_message.Image(base64=base64_url))
@@ -45,20 +101,35 @@ class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConver
class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter):
def __init__(self, message_converter: QQOfficialMessageConverter):
self.message_converter = message_converter
@staticmethod
async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent:
return event.source_platform_object
@staticmethod
async def target2yiri(event: QQOfficialEvent):
async def target2yiri(self, event: QQOfficialEvent):
"""
QQ官方消息转换为LB对象
"""
yiri_chain = await QQOfficialMessageConverter.target2yiri(
# Get message reference if present
message_reference = event.message_reference
# Determine context based on event type
channel_id = event.channel_id if event.t in ['AT_MESSAGE_CREATE', 'DIRECT_MESSAGE_CREATE'] else None
group_openid = event.group_openid if event.t == 'GROUP_AT_MESSAGE_CREATE' else None
user_openid = event.user_openid if event.t == 'C2C_MESSAGE_CREATE' else None
yiri_chain = await self.message_converter.target2yiri(
message=event.content,
message_id=event.d_id,
pic_url=event.attachments,
content_type=event.content_type,
message_reference=message_reference,
event_type=event.t,
channel_id=channel_id,
group_openid=group_openid,
user_openid=user_openid,
)
if event.t == 'C2C_MESSAGE_CREATE':
@@ -135,20 +206,27 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
config: dict
bot_account_id: str
bot_uuid: str = None
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
message_converter: QQOfficialMessageConverter
event_converter: QQOfficialEventConverter
def __init__(self, config: dict, logger: EventLogger):
bot = QQOfficialClient(
app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger, unified_mode=True
)
# Initialize converters with bot reference
message_converter = QQOfficialMessageConverter(bot=bot)
event_converter = QQOfficialEventConverter(message_converter=message_converter)
super().__init__(
config=config,
logger=logger,
bot=bot,
bot_account_id=config['appid'],
)
self.message_converter = message_converter
self.event_converter = event_converter
async def reply_message(
self,

View File

@@ -1,398 +0,0 @@
from __future__ import annotations
import typing
import asyncio
import traceback
import base64
import datetime
import aiohttp
import pydantic
from botbuilder.core import BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext
from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, ConversationAccount
from botframework.connector.auth import MicrosoftAppCredentials
from quart import Quart, request, Response
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
import langbot_plugin.api.entities.builtin.platform.events as platform_events
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
class TeamsMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
"""Convert messages between LangBot format and Teams Activity format"""
@staticmethod
async def yiri2target(message_chain: platform_message.MessageChain) -> dict:
"""Convert LangBot message chain to Teams message format"""
text_content = []
attachments = []
for component in message_chain:
if isinstance(component, platform_message.Plain):
text_content.append(component.text)
elif isinstance(component, platform_message.Image):
# Teams supports image attachments
image_data = None
image_type = 'image/png'
if component.base64:
# Extract base64 data
if component.base64.startswith('data:'):
parts = component.base64.split(',')
header = parts[0]
if 'image/' in header:
image_type = header.split(';')[0].replace('data:', '')
image_data = parts[1] if len(parts) > 1 else component.base64
else:
image_data = component.base64
elif component.url:
# For URLs, Teams can display them inline
text_content.append(f'\n{component.url}')
continue
elif component.path:
try:
with open(component.path, 'rb') as f:
image_bytes = f.read()
image_data = base64.b64encode(image_bytes).decode('utf-8')
except Exception:
continue
if image_data:
# Create inline image attachment
attachments.append({
'contentType': image_type,
'contentUrl': f'data:{image_type};base64,{image_data}',
'name': 'image'
})
result = {
'text': ''.join(text_content),
}
if attachments:
result['attachments'] = attachments
return result
@staticmethod
async def target2yiri(activity: Activity) -> platform_message.MessageChain:
"""Convert Teams Activity to LangBot message chain"""
components = []
# Add message source
components.append(
platform_message.Source(
id=activity.id,
time=activity.timestamp if activity.timestamp else datetime.datetime.now()
)
)
# Handle mentions (convert to At components)
if activity.entities:
for entity in activity.entities:
if entity.type == 'mention':
mentioned = entity.mentioned
if mentioned:
components.append(platform_message.At(target=str(mentioned.id)))
# Add text content
if activity.text:
text = activity.text
# Remove bot mentions from text
if activity.entities:
for entity in activity.entities:
if entity.type == 'mention' and entity.text:
text = text.replace(entity.text, '').strip()
if text:
components.append(platform_message.Plain(text=text))
# Handle attachments (images, files, etc.)
if activity.attachments:
for attachment in activity.attachments:
if attachment.content_type and 'image' in attachment.content_type:
# Download and convert image to base64
if attachment.content_url:
try:
async with aiohttp.ClientSession() as session:
async with session.get(attachment.content_url) as response:
image_data = await response.read()
image_base64 = base64.b64encode(image_data).decode('utf-8')
content_type = attachment.content_type or 'image/png'
components.append(
platform_message.Image(
base64=f'data:{content_type};base64,{image_base64}'
)
)
except Exception:
pass
return platform_message.MessageChain(components)
class TeamsEventConverter(abstract_platform_adapter.AbstractEventConverter):
"""Convert events between Teams Activity and LangBot platform events"""
@staticmethod
async def yiri2target(event: platform_events.Event) -> Activity:
"""Convert LangBot event to Teams Activity"""
return event.source_platform_object
@staticmethod
async def target2yiri(activity: Activity, bot_id: str) -> platform_events.Event:
"""Convert Teams Activity to LangBot event"""
message_chain = await TeamsMessageConverter.target2yiri(activity)
# Determine if it's a personal or channel/group chat
conversation_type = activity.conversation.conversation_type if activity.conversation else None
if conversation_type == 'personal':
# Personal chat (1-on-1)
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=activity.from_property.id,
nickname=activity.from_property.name or activity.from_property.id,
remark=activity.from_property.id,
),
message_chain=message_chain,
time=activity.timestamp.timestamp() if activity.timestamp else datetime.datetime.now().timestamp(),
source_platform_object=activity,
)
else:
# Channel or group chat
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=activity.from_property.id,
member_name=activity.from_property.name or activity.from_property.id,
permission=platform_entities.Permission.Member,
group=platform_entities.Group(
id=activity.conversation.id,
name=activity.conversation.name or activity.conversation.id,
permission=platform_entities.Permission.Member,
),
special_title='',
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=message_chain,
time=activity.timestamp.timestamp() if activity.timestamp else datetime.datetime.now().timestamp(),
source_platform_object=activity,
)
class TeamsAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
"""Microsoft Teams platform adapter for LangBot"""
adapter: BotFrameworkAdapter = pydantic.Field(exclude=True, default=None)
app: Quart = pydantic.Field(exclude=True, default=None)
bot_uuid: typing.Optional[str] = None
message_converter: TeamsMessageConverter = TeamsMessageConverter()
event_converter: TeamsEventConverter = TeamsEventConverter()
config: dict
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
] = {}
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs):
"""Initialize Teams adapter with app credentials"""
# Validate required config
if 'app_id' not in config or 'app_password' not in config:
raise ValueError('Teams adapter requires app_id and app_password in configuration')
# Create Bot Framework adapter settings
settings = BotFrameworkAdapterSettings(
app_id=config['app_id'],
app_password=config['app_password']
)
# Create Bot Framework adapter
adapter_instance = BotFrameworkAdapter(settings)
super().__init__(
config=config,
logger=logger,
bot_account_id=config['app_id'],
listeners={},
**kwargs
)
# Set the adapter after initialization
self.adapter = adapter_instance
async def _handle_activity(self, activity: Activity):
"""Internal method to handle incoming activities"""
try:
# Only process message activities from users (not bots)
if activity.type == ActivityTypes.message:
if activity.from_property and not (hasattr(activity.from_property, 'role') and activity.from_property.role == 'bot'):
# Convert to LangBot event
lb_event = await self.event_converter.target2yiri(activity, self.bot_account_id)
# Call appropriate listener
event_type = type(lb_event)
if event_type in self.listeners:
await self.listeners[event_type](lb_event, self)
except Exception as e:
await self.logger.error(f'Error handling Teams activity: {str(e)}\n{traceback.format_exc()}')
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
"""Send a message to a specific target"""
try:
message_data = await self.message_converter.yiri2target(message)
# Create activity
activity = Activity(
type=ActivityTypes.message,
text=message_data.get('text', ''),
attachments=message_data.get('attachments', []),
conversation=ConversationAccount(id=target_id),
)
# Send via Bot Framework adapter
# Note: This requires a conversation reference which we would need to store
# For now, this is a placeholder - full implementation would require conversation tracking
await self.logger.warning('Direct send_message not fully implemented - use reply_message instead')
except Exception as e:
await self.logger.error(f'Error sending Teams message: {str(e)}')
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
"""Reply to a message"""
try:
assert isinstance(message_source.source_platform_object, Activity)
source_activity: Activity = message_source.source_platform_object
message_data = await self.message_converter.yiri2target(message)
# Build conversation reference from source activity
conversation_reference = TurnContext.get_conversation_reference(source_activity)
# Create reply activity
async def send_reply(turn_context: TurnContext):
reply_text = message_data.get('text', '')
reply_attachments = message_data.get('attachments', [])
if quote_origin:
# Include reply_to_id to quote the original message
await turn_context.send_activity(
Activity(
type=ActivityTypes.message,
text=reply_text,
attachments=reply_attachments,
reply_to_id=source_activity.id
)
)
else:
await turn_context.send_activity(
Activity(
type=ActivityTypes.message,
text=reply_text,
attachments=reply_attachments
)
)
# Continue conversation using the conversation reference
await self.adapter.continue_conversation(
conversation_reference,
send_reply,
self.bot_account_id
)
except Exception as e:
await self.logger.error(f'Error replying to Teams message: {str(e)}\n{traceback.format_exc()}')
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
],
):
"""Register event listener"""
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
_callback: typing.Callable[
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
],
):
"""Unregister event listener"""
if event_type in self.listeners:
del self.listeners[event_type]
def set_bot_uuid(self, bot_uuid: str):
"""Set bot UUID for webhook URL generation"""
self.bot_uuid = bot_uuid
async def handle_unified_webhook(self, _bot_uuid: str, _path: str, request_obj):
"""Handle unified webhook requests from Teams
Args:
_bot_uuid: Bot UUID (unused)
_path: Sub-path (unused)
request_obj: Quart Request object
Returns:
Response data
"""
try:
# Get request body
body = await request_obj.get_json()
# Get authorization header
auth_header = request_obj.headers.get('Authorization', '')
# Process activity
async def bot_logic(turn_context: TurnContext):
# Handle the activity
await self._handle_activity(turn_context.activity)
# Process the request through Bot Framework adapter
await self.adapter.process_activity(body, auth_header, bot_logic)
return Response(status=200)
except Exception as e:
await self.logger.error(f'Error processing Teams webhook: {str(e)}\n{traceback.format_exc()}')
return Response(status=500)
async def run_async(self):
"""Run the adapter - Teams uses webhook mode, so just keep alive and log webhook URL"""
# Print webhook callback address
if self.bot_uuid and hasattr(self.logger, 'ap'):
try:
api_port = self.logger.ap.instance_config.data['api']['port']
webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}"
webhook_url_public = f"http://<Your-Public-IP>:{api_port}/bots/{self.bot_uuid}"
await self.logger.info(f"Teams Bot Webhook URL:")
await self.logger.info(f" Local: {webhook_url}")
await self.logger.info(f" Public: {webhook_url_public}")
await self.logger.info(f"Configure this URL as the messaging endpoint in Azure Bot Service")
except Exception as e:
await self.logger.warning(f"Could not generate webhook URL: {e}")
# Keep the adapter running
async def keep_alive():
while True:
await asyncio.sleep(1)
await keep_alive()
async def kill(self) -> bool:
"""Shutdown the adapter"""
await self.logger.info('Teams adapter stopped')
return True

View File

@@ -1,37 +0,0 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: teams
label:
en_US: Microsoft Teams
zh_Hans: 微软 Teams
description:
en_US: Microsoft Teams Adapter - supports personal and channel/group chats
zh_Hans: 微软 Teams 适配器,支持个人聊天和频道/群组聊天
icon: teams.svg
spec:
config:
- name: app_id
label:
en_US: App ID
zh_Hans: 应用 ID
description:
en_US: Microsoft App ID from Azure Bot registration
zh_Hans: Azure Bot 注册的 Microsoft 应用 ID
type: string
required: true
default: ""
- name: app_password
label:
en_US: App Password
zh_Hans: 应用密码
description:
en_US: Microsoft App Password (Client Secret) from Azure Bot registration
zh_Hans: Azure Bot 注册的 Microsoft 应用密码(客户端密钥)
type: string
required: true
default: ""
execution:
python:
path: ./teams.py
attr: TeamsAdapter