ForcePilot/backend/package/yuxi/channel/extensions/zulip/outbound.py
Kris d5e36d33b7 feat(channel): 添加 Zalo OA、Zoom Chat 和 Zulip 渠道扩展
新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。

Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools

Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading

Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
2026-05-21 12:06:26 +08:00

128 lines
4.2 KiB
Python

from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from yuxi.channel.extensions.zulip.client import ZulipAsyncClient
from yuxi.channel.extensions.zulip.format import safe_split_zulip_content, truncate_zulip_content
from yuxi.channel.extensions.zulip.types import OutboundResult
logger = logging.getLogger(__name__)
def parse_target_id(target_id: str) -> tuple[str, str | list[str]]:
if target_id.startswith("stream:"):
stream_name = target_id[len("stream:") :]
return "stream", stream_name
elif target_id.startswith("private:"):
user_email = target_id[len("private:") :]
return "private", [user_email]
else:
raise ValueError(f"Invalid target_id: {target_id}")
class ZulipOutbound:
async def send_text(
self,
client: ZulipAsyncClient,
target_id: str,
content: str,
*,
reply_to_id: str | None = None,
thread_id: str | None = None,
) -> OutboundResult:
chat_type, destination = parse_target_id(target_id)
chunks = safe_split_zulip_content(content)
last_result: OutboundResult | None = None
for chunk in chunks:
truncated = truncate_zulip_content(chunk)
payload: dict[str, Any] = {
"type": chat_type,
"to": destination,
"content": truncated,
}
if chat_type == "stream" and thread_id:
payload["topic"] = thread_id
try:
result = await client.send_message(payload)
last_result = OutboundResult(
message_id=str(result.get("id", "")),
success=result.get("result") == "success",
raw=result,
)
except Exception as exc:
logger.error("Failed to send text to %s: %s", target_id, exc)
last_result = OutboundResult(
success=False,
error=str(exc),
)
return last_result or OutboundResult(success=False, error="no chunks sent")
async def send_media(
self,
client: ZulipAsyncClient,
target_id: str,
file_path: str,
media_type: str,
*,
thread_id: str | None = None,
) -> OutboundResult:
try:
upload_result = await client.upload_file(file_path)
file_url = upload_result.get("url", "")
file_name = Path(file_path).name
if media_type.startswith("image/"):
markdown = f"[{file_name}]({file_url})"
else:
markdown = f"[📎 {file_name}]({file_url})"
return await self.send_text(
client,
target_id,
markdown,
thread_id=thread_id,
)
except Exception as exc:
logger.error("Failed to send media to %s: %s", target_id, exc)
return OutboundResult(success=False, error=str(exc))
async def edit_message(
self,
client: ZulipAsyncClient,
message_id: int,
content: str,
) -> OutboundResult:
try:
truncated = truncate_zulip_content(content)
result = await client.update_message(message_id, truncated)
return OutboundResult(
message_id=str(message_id),
success=result.get("result") == "success",
raw=result,
)
except Exception as exc:
logger.error("Failed to edit message %s: %s", message_id, exc)
return OutboundResult(success=False, error=str(exc))
async def delete_message(
self,
client: ZulipAsyncClient,
message_id: int,
) -> OutboundResult:
try:
result = await client.delete_message(message_id)
return OutboundResult(
message_id=str(message_id),
success=result.get("result") == "success",
raw=result,
)
except Exception as exc:
logger.error("Failed to delete message %s: %s", message_id, exc)
return OutboundResult(success=False, error=str(exc))