ForcePilot/backend/package/yuxi/channel/extensions/feishu/card_interaction.py
Kris 5e91bb9985 feat(feishu): 新增飞书渠道完整插件实现
新增飞书渠道插件,包含基础配置、事件处理、消息收发、工具调用、权限管理等完整功能模块,支持WebSocket长连接、卡片消息、流式响应、群管理、审批通知等能力
2026-05-21 10:46:42 +08:00

120 lines
3.7 KiB
Python

from __future__ import annotations
import json
import logging
import time
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class CardInteractionEnvelope:
version: str = "ocf1"
kind: str = "button"
action: str = ""
quick_response: str | None = None
metadata: dict | None = None
context: dict | None = None
class CardInteractionResult:
def __init__(self, status: str, action: str = "", envelope: CardInteractionEnvelope | None = None):
self.status = status
self.action = action
self.envelope = envelope
def build_card_interaction_envelope(
action: str,
kind: str = "button",
user_id: str | None = None,
chat_id: str | None = None,
ttl_ms: int = 300_000,
) -> CardInteractionEnvelope:
context = {}
if user_id:
context["u"] = user_id
if chat_id:
context["h"] = chat_id
context["e"] = int(time.time() * 1000) + ttl_ms
context["t"] = int(time.time() * 1000)
return CardInteractionEnvelope(
version="ocf1",
kind=kind,
action=action,
context=context,
)
def encode_envelope_value(envelope: CardInteractionEnvelope) -> str:
parts = [f"oc:{envelope.version}"]
parts.append(f"k:{envelope.kind}")
parts.append(f"a:{envelope.action}")
if envelope.quick_response:
parts.append(f"q:{envelope.quick_response}")
if envelope.metadata:
parts.append(f"m:{json.dumps(envelope.metadata, ensure_ascii=False)}")
if envelope.context:
parts.append(f"c:{json.dumps(envelope.context, ensure_ascii=False)}")
return ";".join(parts)
def decode_card_action(value: str, *, user_id: str | None = None, chat_id: str | None = None) -> CardInteractionResult:
if not value or not value.startswith("oc:"):
if "command" in value or "text" in value:
try:
data = json.loads(value)
return CardInteractionResult(status="legacy", action=data.get("command", data.get("text", "")))
except (json.JSONDecodeError, TypeError):
pass
return CardInteractionResult(status="invalid")
parts = {}
for part in value.split(";"):
if ":" in part:
key, val = part.split(":", 1)
parts[key] = val
version = parts.get("oc", "")
if version != "ocf1":
return CardInteractionResult(status="invalid")
context_str = parts.get("c", "{}")
try:
context = json.loads(context_str)
except (json.JSONDecodeError, TypeError):
return CardInteractionResult(status="invalid")
if user_id and context.get("u") and context["u"] != user_id:
return CardInteractionResult(status="invalid: wrong_user")
if chat_id and context.get("h") and context["h"] != chat_id:
return CardInteractionResult(status="invalid: wrong_conversation")
expiry = context.get("e", 0)
if expiry and int(time.time() * 1000) > expiry:
return CardInteractionResult(status="invalid: stale")
envelope = CardInteractionEnvelope(
version=version,
kind=parts.get("k", "button"),
action=parts.get("a", ""),
quick_response=parts.get("q"),
metadata=json.loads(parts.get("m", "{}")) if parts.get("m") else None,
context=context,
)
return CardInteractionResult(status="structured", action=envelope.action, envelope=envelope)
def check_legacy_card_command(card_body: dict) -> bool:
elements = card_body.get("elements", [])
for elem in elements:
if elem.get("tag") in ("button", "overflow", "select_static"):
value = elem.get("value", "")
if value and not value.startswith("oc:"):
if "command" in value or "text" in value:
return True
return False