ForcePilot/backend/package/yuxi/channel/extensions/flock/outbound.py
Kris 0bad70ec19 feat(flock): 新增Flock团队协作IM渠道插件
实现了完整的Flock渠道接入能力,包含消息收发、Webhook事件监听、账号配置、安全校验、媒体文件处理等功能,支持私聊和群组聊天,适配ForcePilot插件规范。
2026-05-21 10:47:01 +08:00

213 lines
6.7 KiB
Python

from __future__ import annotations
import logging
from .config import _apply_env_overrides, _dict_to_account
from .constants import (
ENDPOINT_SEND_MESSAGE,
MAX_TEXT_LENGTH,
)
from .format import chunk_text, markdown_to_flockml, strip_markdown_for_plain
from .types import FlockOutboundResult
from .utils import create_http_client, flock_api_call
logger = logging.getLogger(__name__)
text_chunk_limit = MAX_TEXT_LENGTH
def chunker(text: str, limit: int, ctx: object | None = None) -> list[str]:
return chunk_text(text, max(limit, 100))
async def send_text(
target_id: str,
content: str,
*,
reply_to_id: str | None = None,
thread_id: str | None = None,
mention_ids: list[str] | None = None,
notification: str | None = None,
send_as: dict | None = None,
on_behalf_of: str | None = None,
visible_to: list[str] | None = None,
account_id: str = "default",
config: dict,
) -> FlockOutboundResult:
account_data = config.get("accounts", {}).get(account_id, {})
account = _dict_to_account(account_data)
account = _apply_env_overrides(account)
if not account.bot_token and not account.incoming_webhook_url:
return FlockOutboundResult(
success=False,
error="No valid send method configured (bot_token or incoming_webhook_url required)",
)
flockml = markdown_to_flockml(content)
plain_text = strip_markdown_for_plain(content)
if account.bot_token:
return await _send_via_bot_token(
account.bot_token, target_id, plain_text, flockml,
reply_to_id, thread_id, mention_ids, notification, send_as, on_behalf_of, visible_to,
)
if account.incoming_webhook_url:
return await _send_via_webhook(account.incoming_webhook_url, plain_text, flockml)
return FlockOutboundResult(success=False, error="No valid send method configured")
async def _send_via_bot_token(
bot_token: str,
target_id: str,
plain_text: str,
flockml: str,
reply_to_id: str | None,
thread_id: str | None,
mention_ids: list[str] | None,
notification: str | None,
send_as: dict | None,
on_behalf_of: str | None,
visible_to: list[str] | None,
) -> FlockOutboundResult:
payload: dict = {
"to": target_id,
"text": plain_text,
"flockml": flockml,
}
if thread_id:
payload["threadId"] = thread_id
if mention_ids:
payload["mentions"] = mention_ids
if notification:
payload["notification"] = notification
if send_as:
payload["sendAs"] = send_as
if on_behalf_of:
payload["onBehalfOf"] = on_behalf_of
if visible_to:
payload["visibleTo"] = visible_to
client = create_http_client()
try:
result = await flock_api_call(client, ENDPOINT_SEND_MESSAGE, bot_token, payload)
msg_uid = result.get("uid", "")
logger.info("Flock message sent to %s, uid=%s", target_id, msg_uid)
return FlockOutboundResult(success=True, message_uid=msg_uid)
except Exception as e:
logger.error("Flock send via bot token failed: %s", e)
retryable = getattr(e, "retryable", False)
return FlockOutboundResult(success=False, error=str(e), retryable=retryable)
finally:
await client.aclose()
async def _send_via_webhook(
incoming_webhook_url: str,
plain_text: str,
flockml: str,
) -> FlockOutboundResult:
client = create_http_client()
try:
resp = await client.post(
incoming_webhook_url,
json={"text": plain_text, "flockml": flockml},
)
if resp.status_code == 200:
logger.info("Flock message sent via incoming webhook")
return FlockOutboundResult(success=True)
logger.warning("Flock webhook send failed: HTTP %d", resp.status_code)
return FlockOutboundResult(
success=False,
error=f"Incoming webhook returned HTTP {resp.status_code}",
)
except Exception as e:
logger.error("Flock webhook send error: %s", e)
return FlockOutboundResult(success=False, error=str(e))
finally:
await client.aclose()
async def send_media(
target_id: str,
media_url: str,
media_type: str,
*,
reply_to_id: str | None = None,
thread_id: str | None = None,
account_id: str = "default",
config: dict,
) -> FlockOutboundResult:
account_data = config.get("accounts", {}).get(account_id, {})
account = _dict_to_account(account_data)
account = _apply_env_overrides(account)
if not account.bot_token:
return FlockOutboundResult(success=False, error="bot_token required for media upload")
if media_type == "image":
payload: dict = {
"to": target_id,
"text": "",
"attachments": [
{
"title": "Image",
"views": {
"image": {
"original": {"src": media_url},
"thumbnail": {"src": media_url},
}
},
}
],
}
if thread_id:
payload["threadId"] = thread_id
client = create_http_client()
try:
result = await flock_api_call(client, ENDPOINT_SEND_MESSAGE, account.bot_token, payload)
return FlockOutboundResult(success=True, message_uid=result.get("uid", ""))
except Exception as e:
logger.error("Flock image send failed: %s", e)
retryable = getattr(e, "retryable", False)
return FlockOutboundResult(success=False, error=str(e), retryable=retryable)
finally:
await client.aclose()
return FlockOutboundResult(
success=False,
error=f"Unsupported media type: {media_type}. Use 'image' or use media.upload_file for files.",
)
async def send_payload(
ctx: object,
config: dict,
account_id: str = "default",
) -> FlockOutboundResult:
target_id = getattr(ctx, "target_id", "")
content = getattr(ctx, "content", "")
thread_id = getattr(ctx, "thread_id", None)
reply_to_id = getattr(ctx, "reply_to_id", None)
mention_ids = getattr(ctx, "mention_ids", None)
notification = getattr(ctx, "notification", None)
send_as = getattr(ctx, "send_as", None)
on_behalf_of = getattr(ctx, "on_behalf_of", None)
visible_to = getattr(ctx, "visible_to", None)
return await send_text(
target_id,
content,
reply_to_id=reply_to_id,
thread_id=thread_id,
mention_ids=mention_ids,
notification=notification,
send_as=send_as,
on_behalf_of=on_behalf_of,
visible_to=visible_to,
account_id=account_id,
config=config,
)