Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
97 lines
3.8 KiB
Python
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)
|