ForcePilot/backend/package/yuxi/channel/capabilities/negotiation.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

123 lines
3.8 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from yuxi.channel.capabilities.levels import (
EditSupport,
InteractiveSupport,
MarkdownSupport,
MediaSupport,
StreamingSupport,
ThreadSupport,
)
from yuxi.channel.ports import AuthPort, InboundPort, MetaPort, OutboundPort, SessionPort
@dataclass
class CapabilityRequirement:
capability: str
level: str | None = None
fallback: str = "ignore"
@dataclass
class NegotiationResult:
supported: bool
fallback: str
reason: str | None = None
_LEVEL_ORDER = {"none": 0, "basic": 1, "extended": 2, "full": 3}
def supports(channel: MetaPort, capability: str, config: dict | None = None) -> bool:
try:
matrix = channel.get_meta().capability_matrix
except AttributeError:
return False
if matrix is None:
return False
return matrix.supports(capability, config)
def negotiate(channel: MetaPort, requirement: CapabilityRequirement) -> NegotiationResult:
if not supports(channel, requirement.capability):
return NegotiationResult(
supported=False,
fallback=requirement.fallback,
reason=f"Channel does not support '{requirement.capability}'",
)
if requirement.level is not None and not _level_met(channel, requirement):
return NegotiationResult(
supported=False,
fallback=requirement.fallback,
reason=f"Capability '{requirement.capability}' supported but level '{requirement.level}' not met",
)
return NegotiationResult(supported=True, fallback=requirement.fallback)
def _level_met(channel: MetaPort, requirement: CapabilityRequirement) -> bool:
matrix = channel.get_meta().capability_matrix
cap = requirement.capability
level = requirement.level.lower() if requirement.level else None
if cap in {"text", "reactions", "batch_send", "qr_login", "scan_pairing", "pin", "directory"}:
return level == "enabled"
if cap == "markdown":
return _ordered_level_met(level, matrix.markdown, MarkdownSupport)
if cap == "interactive":
return _ordered_level_met(level, matrix.interactive, InteractiveSupport)
if cap == "threads":
return _ordered_level_met(level, matrix.threads, ThreadSupport)
if cap == "edits":
return _ordered_level_met(level, matrix.edits, EditSupport)
if cap == "streaming":
return _ordered_level_met(level, matrix.streaming, StreamingSupport)
if cap == "media":
if level is None:
return True
try:
required = MediaSupport(level.upper())
except ValueError:
return False
return required in matrix.media and required != MediaSupport.NONE
return True
def _ordered_level_met(
level: str | None,
current: MarkdownSupport | InteractiveSupport | ThreadSupport | EditSupport | StreamingSupport,
enum_cls: type,
) -> bool:
if level is None:
return True
required_order = _LEVEL_ORDER.get(level)
if required_order is None:
return False
current_order = _LEVEL_ORDER.get(current.value.lower(), 0)
return current_order >= required_order and current != enum_cls.NONE
def get_required_ports(capability: str) -> set[type]:
mapping: dict[str, set[type]] = {
"text": {OutboundPort},
"markdown": {OutboundPort},
"media": {OutboundPort},
"interactive": {OutboundPort},
"reactions": {OutboundPort},
"threads": {OutboundPort, SessionPort},
"edits": {OutboundPort},
"streaming": {OutboundPort},
"batch_send": {OutboundPort},
"qr_login": {AuthPort},
"scan_pairing": {InboundPort},
"pin": {OutboundPort},
"directory": {OutboundPort},
}
return mapping.get(capability, set())