Files
shirobana-bot/bot-utility/agent/capture.py
T
RRRosmontis 34089d0042 Initial import of Shirobana bot backend
Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
2026-11-11 00:25:30 +08:00

97 lines
3.8 KiB
Python

"""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)