"""Non-delivering channel adapter used around legacy command handlers.""" from __future__ import annotations from typing import Any, Dict, List from .artifacts import ArtifactError, ArtifactStore MEDIA_TYPES = frozenset(("image", "file", "record", "video")) class CaptureChannel: """Capture text and OneBot media segments instead of sending them. Legacy modules call ``channel.send_message(CaptureChannel, ...)``. Returning ``False`` from ``send`` is intentional: the shared sender then skips its private-message mirror fan-out, preventing a second real reply. Tool media delivery is performed separately and recorded by ``ToolExecutor``. """ def __init__(self, context: Any, artifacts: ArtifactStore) -> None: self._context = context self._real_channel = context.channel self._artifacts = artifacts self.name = getattr(context.channel, "name", "unknown") self.texts: List[str] = [] self.media: List[Dict[str, Any]] = [] self.messages: List[Any] = [] self.capture_errors: List[str] = [] def __getattr__(self, name: str) -> Any: """Preserve channel metadata used by legacy modules (for example TG flags).""" return getattr(self._real_channel, name) def is_admin(self, user_id: Any) -> bool: """Use the already validated execution context, never mutable handler state.""" return bool(self._context.is_admin) async def send(self, msg_type: str, target_id: int, content: Any) -> bool: """Record one legacy send operation; never deliver it to the real channel.""" self.messages.append(content) if isinstance(content, str): self.texts.append(content) return False if not isinstance(content, (list, tuple)): self.texts.append(str(content)) return False for segment in content: if not isinstance(segment, dict): continue segment_type = segment.get("type") data = segment.get("data") if not isinstance(data, dict): data = {} if segment_type == "text": text = data.get("text") if text is not None: self.texts.append(str(text)) continue if segment_type not in MEDIA_TYPES: continue source = data.get("file") or data.get("url") if not source: self.capture_errors.append("media segment has no source") continue try: artifact = self._artifacts.register( source, media_type=segment_type, filename=data.get("name"), ctx=self._context, ) except (ArtifactError, OSError, ValueError): self.capture_errors.append("media could not be registered") continue self.media.append({ "type": segment_type, "artifact_id": artifact["artifact_id"], "data": dict(data), }) # Suppress channel._mirror_private; this is capture, not a failed tool. return False @property def artifact_ids(self) -> List[str]: """Return IDs in capture order, de-duplicated for compact result metadata.""" return list(dict.fromkeys(item["artifact_id"] for item in self.media)) def public_artifacts(self) -> List[Dict[str, Any]]: """Return safe metadata for all successfully captured media.""" return [self._artifacts.get(item["artifact_id"], self._context) for item in self.media] def safe_text(self) -> str: """Join captured text for subsequent model rewriting.""" return "\n".join(text for text in self.texts if text)