refactor(channels): rename moltchat integration to mochat

This commit is contained in:
tjb-tech 2026-02-09 08:50:17 +00:00
parent 20b8a2fc58
commit 3779225917
7 changed files with 92 additions and 92 deletions

View File

@ -164,7 +164,7 @@ nanobot agent -m "Hello from my local LLM!"
## 💬 Chat Apps ## 💬 Chat Apps
Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu, or Moltchat — anytime, anywhere. Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu, or Mochat — anytime, anywhere.
| Channel | Setup | | Channel | Setup |
|---------|-------| |---------|-------|
@ -172,7 +172,7 @@ Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu, or Moltchat
| **Discord** | Easy (bot token + intents) | | **Discord** | Easy (bot token + intents) |
| **WhatsApp** | Medium (scan QR) | | **WhatsApp** | Medium (scan QR) |
| **Feishu** | Medium (app credentials) | | **Feishu** | Medium (app credentials) |
| **Moltchat** | Medium (claw token + websocket) | | **Mochat** | Medium (claw token + websocket) |
<details> <details>
<summary><b>Telegram</b> (Recommended)</summary> <summary><b>Telegram</b> (Recommended)</summary>
@ -207,7 +207,7 @@ nanobot gateway
</details> </details>
<details> <details>
<summary><b>Moltchat (Claw IM)</b></summary> <summary><b>Mochat (Claw IM)</b></summary>
Uses **Socket.IO WebSocket** by default, with HTTP polling fallback. Uses **Socket.IO WebSocket** by default, with HTTP polling fallback.
@ -221,7 +221,7 @@ Uses **Socket.IO WebSocket** by default, with HTTP polling fallback.
```json ```json
{ {
"channels": { "channels": {
"moltchat": { "mochat": {
"enabled": true, "enabled": true,
"baseUrl": "https://mochat.io", "baseUrl": "https://mochat.io",
"socketUrl": "https://mochat.io", "socketUrl": "https://mochat.io",
@ -244,7 +244,7 @@ nanobot gateway
``` ```
> [!TIP] > [!TIP]
> Keep `clawToken` private. It should only be sent in `X-Claw-Token` header to your Moltchat API endpoint. > Keep `clawToken` private. It should only be sent in `X-Claw-Token` header to your Mochat API endpoint.
</details> </details>
@ -456,7 +456,7 @@ docker run -v ~/.nanobot:/root/.nanobot --rm nanobot onboard
# Edit config on host to add API keys # Edit config on host to add API keys
vim ~/.nanobot/config.json vim ~/.nanobot/config.json
# Run gateway (connects to enabled channels, e.g. Telegram/Discord/Moltchat) # Run gateway (connects to enabled channels, e.g. Telegram/Discord/Mochat)
docker run -v ~/.nanobot:/root/.nanobot -p 18790:18790 nanobot gateway docker run -v ~/.nanobot:/root/.nanobot -p 18790:18790 nanobot gateway
# Or run a single command # Or run a single command

View File

@ -2,6 +2,6 @@
from nanobot.channels.base import BaseChannel from nanobot.channels.base import BaseChannel
from nanobot.channels.manager import ChannelManager from nanobot.channels.manager import ChannelManager
from nanobot.channels.moltchat import MoltchatChannel from nanobot.channels.mochat import MochatChannel
__all__ = ["BaseChannel", "ChannelManager", "MoltchatChannel"] __all__ = ["BaseChannel", "ChannelManager", "MochatChannel"]

View File

@ -78,17 +78,17 @@ class ChannelManager:
except ImportError as e: except ImportError as e:
logger.warning(f"Feishu channel not available: {e}") logger.warning(f"Feishu channel not available: {e}")
# Moltchat channel # Mochat channel
if self.config.channels.moltchat.enabled: if self.config.channels.mochat.enabled:
try: try:
from nanobot.channels.moltchat import MoltchatChannel from nanobot.channels.mochat import MochatChannel
self.channels["moltchat"] = MoltchatChannel( self.channels["mochat"] = MochatChannel(
self.config.channels.moltchat, self.bus self.config.channels.mochat, self.bus
) )
logger.info("Moltchat channel enabled") logger.info("Mochat channel enabled")
except ImportError as e: except ImportError as e:
logger.warning(f"Moltchat channel not available: {e}") logger.warning(f"Mochat channel not available: {e}")
async def start_all(self) -> None: async def start_all(self) -> None:
"""Start WhatsApp channel and the outbound dispatcher.""" """Start WhatsApp channel and the outbound dispatcher."""

View File

@ -1,4 +1,4 @@
"""Moltchat channel implementation using Socket.IO with HTTP polling fallback.""" """Mochat channel implementation using Socket.IO with HTTP polling fallback."""
from __future__ import annotations from __future__ import annotations
@ -15,7 +15,7 @@ from loguru import logger
from nanobot.bus.events import OutboundMessage from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel from nanobot.channels.base import BaseChannel
from nanobot.config.schema import MoltchatConfig from nanobot.config.schema import MochatConfig
from nanobot.utils.helpers import get_data_path from nanobot.utils.helpers import get_data_path
try: try:
@ -39,7 +39,7 @@ CURSOR_SAVE_DEBOUNCE_S = 0.5
@dataclass @dataclass
class MoltchatBufferedEntry: class MochatBufferedEntry:
"""Buffered inbound entry for delayed dispatch.""" """Buffered inbound entry for delayed dispatch."""
raw_body: str raw_body: str
@ -55,20 +55,20 @@ class MoltchatBufferedEntry:
class DelayState: class DelayState:
"""Per-target delayed message state.""" """Per-target delayed message state."""
entries: list[MoltchatBufferedEntry] = field(default_factory=list) entries: list[MochatBufferedEntry] = field(default_factory=list)
lock: asyncio.Lock = field(default_factory=asyncio.Lock) lock: asyncio.Lock = field(default_factory=asyncio.Lock)
timer: asyncio.Task | None = None timer: asyncio.Task | None = None
@dataclass @dataclass
class MoltchatTarget: class MochatTarget:
"""Outbound target resolution result.""" """Outbound target resolution result."""
id: str id: str
is_panel: bool is_panel: bool
def normalize_moltchat_content(content: Any) -> str: def normalize_mochat_content(content: Any) -> str:
"""Normalize content payload to text.""" """Normalize content payload to text."""
if isinstance(content, str): if isinstance(content, str):
return content.strip() return content.strip()
@ -80,17 +80,17 @@ def normalize_moltchat_content(content: Any) -> str:
return str(content) return str(content)
def resolve_moltchat_target(raw: str) -> MoltchatTarget: def resolve_mochat_target(raw: str) -> MochatTarget:
"""Resolve id and target kind from user-provided target string.""" """Resolve id and target kind from user-provided target string."""
trimmed = (raw or "").strip() trimmed = (raw or "").strip()
if not trimmed: if not trimmed:
return MoltchatTarget(id="", is_panel=False) return MochatTarget(id="", is_panel=False)
lowered = trimmed.lower() lowered = trimmed.lower()
cleaned = trimmed cleaned = trimmed
forced_panel = False forced_panel = False
prefixes = ["moltchat:", "mochat:", "group:", "channel:", "panel:"] prefixes = ["mochat:", "group:", "channel:", "panel:"]
for prefix in prefixes: for prefix in prefixes:
if lowered.startswith(prefix): if lowered.startswith(prefix):
cleaned = trimmed[len(prefix) :].strip() cleaned = trimmed[len(prefix) :].strip()
@ -99,9 +99,9 @@ def resolve_moltchat_target(raw: str) -> MoltchatTarget:
break break
if not cleaned: if not cleaned:
return MoltchatTarget(id="", is_panel=False) return MochatTarget(id="", is_panel=False)
return MoltchatTarget(id=cleaned, is_panel=forced_panel or not cleaned.startswith("session_")) return MochatTarget(id=cleaned, is_panel=forced_panel or not cleaned.startswith("session_"))
def extract_mention_ids(value: Any) -> list[str]: def extract_mention_ids(value: Any) -> list[str]:
@ -152,7 +152,7 @@ def resolve_was_mentioned(payload: dict[str, Any], agent_user_id: str) -> bool:
def resolve_require_mention( def resolve_require_mention(
config: MoltchatConfig, config: MochatConfig,
session_id: str, session_id: str,
group_id: str, group_id: str,
) -> bool: ) -> bool:
@ -167,7 +167,7 @@ def resolve_require_mention(
return bool(config.mention.require_in_groups) return bool(config.mention.require_in_groups)
def build_buffered_body(entries: list[MoltchatBufferedEntry], is_group: bool) -> str: def build_buffered_body(entries: list[MochatBufferedEntry], is_group: bool) -> str:
"""Build text body from one or more buffered entries.""" """Build text body from one or more buffered entries."""
if not entries: if not entries:
return "" return ""
@ -200,20 +200,20 @@ def parse_timestamp(value: Any) -> int | None:
return None return None
class MoltchatChannel(BaseChannel): class MochatChannel(BaseChannel):
"""Moltchat channel using socket.io with fallback polling workers.""" """Mochat channel using socket.io with fallback polling workers."""
name = "moltchat" name = "mochat"
def __init__(self, config: MoltchatConfig, bus: MessageBus): def __init__(self, config: MochatConfig, bus: MessageBus):
super().__init__(config, bus) super().__init__(config, bus)
self.config: MoltchatConfig = config self.config: MochatConfig = config
self._http: httpx.AsyncClient | None = None self._http: httpx.AsyncClient | None = None
self._socket: Any = None self._socket: Any = None
self._ws_connected = False self._ws_connected = False
self._ws_ready = False self._ws_ready = False
self._state_dir = get_data_path() / "moltchat" self._state_dir = get_data_path() / "mochat"
self._cursor_path = self._state_dir / "session_cursors.json" self._cursor_path = self._state_dir / "session_cursors.json"
self._session_cursor: dict[str, int] = {} self._session_cursor: dict[str, int] = {}
self._cursor_save_task: asyncio.Task | None = None self._cursor_save_task: asyncio.Task | None = None
@ -239,9 +239,9 @@ class MoltchatChannel(BaseChannel):
self._target_locks: dict[str, asyncio.Lock] = {} self._target_locks: dict[str, asyncio.Lock] = {}
async def start(self) -> None: async def start(self) -> None:
"""Start Moltchat channel workers and websocket connection.""" """Start Mochat channel workers and websocket connection."""
if not self.config.claw_token: if not self.config.claw_token:
logger.error("Moltchat claw_token not configured") logger.error("Mochat claw_token not configured")
return return
self._running = True self._running = True
@ -296,7 +296,7 @@ class MoltchatChannel(BaseChannel):
async def send(self, msg: OutboundMessage) -> None: async def send(self, msg: OutboundMessage) -> None:
"""Send outbound message to session or panel.""" """Send outbound message to session or panel."""
if not self.config.claw_token: if not self.config.claw_token:
logger.warning("Moltchat claw_token missing, skip send") logger.warning("Mochat claw_token missing, skip send")
return return
content_parts = [msg.content.strip()] if msg.content and msg.content.strip() else [] content_parts = [msg.content.strip()] if msg.content and msg.content.strip() else []
@ -306,9 +306,9 @@ class MoltchatChannel(BaseChannel):
if not content: if not content:
return return
target = resolve_moltchat_target(msg.chat_id) target = resolve_mochat_target(msg.chat_id)
if not target.id: if not target.id:
logger.warning("Moltchat outbound target is empty") logger.warning("Mochat outbound target is empty")
return return
is_panel = target.is_panel or target.id in self._panel_set is_panel = target.is_panel or target.id in self._panel_set
@ -330,7 +330,7 @@ class MoltchatChannel(BaseChannel):
reply_to=msg.reply_to, reply_to=msg.reply_to,
) )
except Exception as e: except Exception as e:
logger.error(f"Failed to send Moltchat message: {e}") logger.error(f"Failed to send Mochat message: {e}")
def _seed_targets_from_config(self) -> None: def _seed_targets_from_config(self) -> None:
sessions, self._auto_discover_sessions = self._normalize_id_list(self.config.sessions) sessions, self._auto_discover_sessions = self._normalize_id_list(self.config.sessions)
@ -351,7 +351,7 @@ class MoltchatChannel(BaseChannel):
async def _start_socket_client(self) -> bool: async def _start_socket_client(self) -> bool:
if not SOCKETIO_AVAILABLE: if not SOCKETIO_AVAILABLE:
logger.warning("python-socketio not installed, Moltchat using polling fallback") logger.warning("python-socketio not installed, Mochat using polling fallback")
return False return False
serializer = "default" serializer = "default"
@ -385,7 +385,7 @@ class MoltchatChannel(BaseChannel):
async def connect() -> None: async def connect() -> None:
self._ws_connected = True self._ws_connected = True
self._ws_ready = False self._ws_ready = False
logger.info("Moltchat websocket connected") logger.info("Mochat websocket connected")
subscribed = await self._subscribe_all() subscribed = await self._subscribe_all()
self._ws_ready = subscribed self._ws_ready = subscribed
@ -400,13 +400,13 @@ class MoltchatChannel(BaseChannel):
return return
self._ws_connected = False self._ws_connected = False
self._ws_ready = False self._ws_ready = False
logger.warning("Moltchat websocket disconnected") logger.warning("Mochat websocket disconnected")
await self._ensure_fallback_workers() await self._ensure_fallback_workers()
@client.event @client.event
async def connect_error(data: Any) -> None: async def connect_error(data: Any) -> None:
message = str(data) message = str(data)
logger.error(f"Moltchat websocket connect error: {message}") logger.error(f"Mochat websocket connect error: {message}")
@client.on("claw.session.events") @client.on("claw.session.events")
async def on_session_events(payload: dict[str, Any]) -> None: async def on_session_events(payload: dict[str, Any]) -> None:
@ -441,7 +441,7 @@ class MoltchatChannel(BaseChannel):
) )
return True return True
except Exception as e: except Exception as e:
logger.error(f"Failed to connect Moltchat websocket: {e}") logger.error(f"Failed to connect Mochat websocket: {e}")
try: try:
await client.disconnect() await client.disconnect()
except Exception: except Exception:
@ -486,7 +486,7 @@ class MoltchatChannel(BaseChannel):
}, },
) )
if not ack.get("result"): if not ack.get("result"):
logger.error(f"Moltchat subscribeSessions failed: {ack.get('message', 'unknown error')}") logger.error(f"Mochat subscribeSessions failed: {ack.get('message', 'unknown error')}")
return False return False
data = ack.get("data") data = ack.get("data")
@ -516,7 +516,7 @@ class MoltchatChannel(BaseChannel):
}, },
) )
if not ack.get("result"): if not ack.get("result"):
logger.error(f"Moltchat subscribePanels failed: {ack.get('message', 'unknown error')}") logger.error(f"Mochat subscribePanels failed: {ack.get('message', 'unknown error')}")
return False return False
return True return True
@ -544,7 +544,7 @@ class MoltchatChannel(BaseChannel):
try: try:
await self._refresh_targets(subscribe_new=self._ws_ready) await self._refresh_targets(subscribe_new=self._ws_ready)
except Exception as e: except Exception as e:
logger.warning(f"Moltchat refresh failed: {e}") logger.warning(f"Mochat refresh failed: {e}")
if self._fallback_mode: if self._fallback_mode:
await self._ensure_fallback_workers() await self._ensure_fallback_workers()
@ -560,7 +560,7 @@ class MoltchatChannel(BaseChannel):
try: try:
response = await self._list_sessions() response = await self._list_sessions()
except Exception as e: except Exception as e:
logger.warning(f"Moltchat listSessions failed: {e}") logger.warning(f"Mochat listSessions failed: {e}")
return return
sessions = response.get("sessions") sessions = response.get("sessions")
@ -599,7 +599,7 @@ class MoltchatChannel(BaseChannel):
try: try:
response = await self._get_workspace_group() response = await self._get_workspace_group()
except Exception as e: except Exception as e:
logger.warning(f"Moltchat getWorkspaceGroup failed: {e}") logger.warning(f"Mochat getWorkspaceGroup failed: {e}")
return return
raw_panels = response.get("panels") raw_panels = response.get("panels")
@ -683,7 +683,7 @@ class MoltchatChannel(BaseChannel):
except asyncio.CancelledError: except asyncio.CancelledError:
break break
except Exception as e: except Exception as e:
logger.warning(f"Moltchat watch fallback error ({session_id}): {e}") logger.warning(f"Mochat watch fallback error ({session_id}): {e}")
await asyncio.sleep(max(0.1, self.config.retry_delay_ms / 1000.0)) await asyncio.sleep(max(0.1, self.config.retry_delay_ms / 1000.0))
async def _panel_poll_worker(self, panel_id: str) -> None: async def _panel_poll_worker(self, panel_id: str) -> None:
@ -723,7 +723,7 @@ class MoltchatChannel(BaseChannel):
except asyncio.CancelledError: except asyncio.CancelledError:
break break
except Exception as e: except Exception as e:
logger.warning(f"Moltchat panel polling error ({panel_id}): {e}") logger.warning(f"Mochat panel polling error ({panel_id}): {e}")
await asyncio.sleep(sleep_s) await asyncio.sleep(sleep_s)
@ -803,7 +803,7 @@ class MoltchatChannel(BaseChannel):
if message_id and self._remember_message_id(seen_key, message_id): if message_id and self._remember_message_id(seen_key, message_id):
return return
raw_body = normalize_moltchat_content(payload.get("content")) raw_body = normalize_mochat_content(payload.get("content"))
if not raw_body: if not raw_body:
raw_body = "[empty message]" raw_body = "[empty message]"
@ -826,7 +826,7 @@ class MoltchatChannel(BaseChannel):
if require_mention and not was_mentioned and not use_delay: if require_mention and not was_mentioned and not use_delay:
return return
entry = MoltchatBufferedEntry( entry = MochatBufferedEntry(
raw_body=raw_body, raw_body=raw_body,
author=author, author=author,
sender_name=sender_name, sender_name=sender_name,
@ -883,7 +883,7 @@ class MoltchatChannel(BaseChannel):
key: str, key: str,
target_id: str, target_id: str,
target_kind: str, target_kind: str,
entry: MoltchatBufferedEntry, entry: MochatBufferedEntry,
) -> None: ) -> None:
state = self._delay_states.setdefault(key, DelayState()) state = self._delay_states.setdefault(key, DelayState())
@ -912,7 +912,7 @@ class MoltchatChannel(BaseChannel):
target_id: str, target_id: str,
target_kind: str, target_kind: str,
reason: str, reason: str,
entry: MoltchatBufferedEntry | None, entry: MochatBufferedEntry | None,
) -> None: ) -> None:
state = self._delay_states.setdefault(key, DelayState()) state = self._delay_states.setdefault(key, DelayState())
@ -944,7 +944,7 @@ class MoltchatChannel(BaseChannel):
self, self,
target_id: str, target_id: str,
target_kind: str, target_kind: str,
entries: list[MoltchatBufferedEntry], entries: list[MochatBufferedEntry],
was_mentioned: bool, was_mentioned: bool,
) -> None: ) -> None:
if not entries: if not entries:
@ -1092,7 +1092,7 @@ class MoltchatChannel(BaseChannel):
try: try:
data = json.loads(self._cursor_path.read_text("utf-8")) data = json.loads(self._cursor_path.read_text("utf-8"))
except Exception as e: except Exception as e:
logger.warning(f"Failed to read Moltchat cursor file: {e}") logger.warning(f"Failed to read Mochat cursor file: {e}")
return return
cursors = data.get("cursors") if isinstance(data, dict) else None cursors = data.get("cursors") if isinstance(data, dict) else None
@ -1114,14 +1114,14 @@ class MoltchatChannel(BaseChannel):
self._state_dir.mkdir(parents=True, exist_ok=True) self._state_dir.mkdir(parents=True, exist_ok=True)
self._cursor_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", "utf-8") self._cursor_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", "utf-8")
except Exception as e: except Exception as e:
logger.warning(f"Failed to save Moltchat cursor file: {e}") logger.warning(f"Failed to save Mochat cursor file: {e}")
def _base_url(self) -> str: def _base_url(self) -> str:
return self.config.base_url.strip().rstrip("/") return self.config.base_url.strip().rstrip("/")
async def _post_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]: async def _post_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
if not self._http: if not self._http:
raise RuntimeError("Moltchat HTTP client not initialized") raise RuntimeError("Mochat HTTP client not initialized")
url = f"{self._base_url()}{path}" url = f"{self._base_url()}{path}"
response = await self._http.post( response = await self._http.post(
@ -1135,7 +1135,7 @@ class MoltchatChannel(BaseChannel):
text = response.text text = response.text
if not response.is_success: if not response.is_success:
raise RuntimeError(f"Moltchat HTTP {response.status_code}: {text[:200]}") raise RuntimeError(f"Mochat HTTP {response.status_code}: {text[:200]}")
parsed: Any parsed: Any
try: try:
@ -1146,7 +1146,7 @@ class MoltchatChannel(BaseChannel):
if isinstance(parsed, dict) and isinstance(parsed.get("code"), int): if isinstance(parsed, dict) and isinstance(parsed.get("code"), int):
if parsed["code"] != 200: if parsed["code"] != 200:
message = str(parsed.get("message") or parsed.get("name") or "request failed") message = str(parsed.get("message") or parsed.get("name") or "request failed")
raise RuntimeError(f"Moltchat API error: {message} (code={parsed['code']})") raise RuntimeError(f"Mochat API error: {message} (code={parsed['code']})")
data = parsed.get("data") data = parsed.get("data")
return data if isinstance(data, dict) else {} return data if isinstance(data, dict) else {}

View File

@ -376,11 +376,11 @@ def channels_status():
fs_config fs_config
) )
# Moltchat # Mochat
mc = config.channels.moltchat mc = config.channels.mochat
mc_base = mc.base_url or "[dim]not configured[/dim]" mc_base = mc.base_url or "[dim]not configured[/dim]"
table.add_row( table.add_row(
"Moltchat", "Mochat",
"" if mc.enabled else "", "" if mc.enabled else "",
mc_base mc_base
) )

View File

@ -39,18 +39,18 @@ class DiscordConfig(BaseModel):
intents: int = 37377 # GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT intents: int = 37377 # GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT
class MoltchatMentionConfig(BaseModel): class MochatMentionConfig(BaseModel):
"""Moltchat mention behavior configuration.""" """Mochat mention behavior configuration."""
require_in_groups: bool = False require_in_groups: bool = False
class MoltchatGroupRule(BaseModel): class MochatGroupRule(BaseModel):
"""Moltchat per-group mention requirement.""" """Mochat per-group mention requirement."""
require_mention: bool = False require_mention: bool = False
class MoltchatConfig(BaseModel): class MochatConfig(BaseModel):
"""Moltchat channel configuration.""" """Mochat channel configuration."""
enabled: bool = False enabled: bool = False
base_url: str = "http://localhost:11000" base_url: str = "http://localhost:11000"
socket_url: str = "" socket_url: str = ""
@ -69,8 +69,8 @@ class MoltchatConfig(BaseModel):
sessions: list[str] = Field(default_factory=list) sessions: list[str] = Field(default_factory=list)
panels: list[str] = Field(default_factory=list) panels: list[str] = Field(default_factory=list)
allow_from: list[str] = Field(default_factory=list) allow_from: list[str] = Field(default_factory=list)
mention: MoltchatMentionConfig = Field(default_factory=MoltchatMentionConfig) mention: MochatMentionConfig = Field(default_factory=MochatMentionConfig)
groups: dict[str, MoltchatGroupRule] = Field(default_factory=dict) groups: dict[str, MochatGroupRule] = Field(default_factory=dict)
reply_delay_mode: str = "non-mention" # off | non-mention reply_delay_mode: str = "non-mention" # off | non-mention
reply_delay_ms: int = 120000 reply_delay_ms: int = 120000
@ -81,7 +81,7 @@ class ChannelsConfig(BaseModel):
telegram: TelegramConfig = Field(default_factory=TelegramConfig) telegram: TelegramConfig = Field(default_factory=TelegramConfig)
discord: DiscordConfig = Field(default_factory=DiscordConfig) discord: DiscordConfig = Field(default_factory=DiscordConfig)
feishu: FeishuConfig = Field(default_factory=FeishuConfig) feishu: FeishuConfig = Field(default_factory=FeishuConfig)
moltchat: MoltchatConfig = Field(default_factory=MoltchatConfig) mochat: MochatConfig = Field(default_factory=MochatConfig)
class AgentDefaults(BaseModel): class AgentDefaults(BaseModel):

View File

@ -1,27 +1,27 @@
import pytest import pytest
from nanobot.bus.queue import MessageBus from nanobot.bus.queue import MessageBus
from nanobot.channels.moltchat import ( from nanobot.channels.mochat import (
MoltchatBufferedEntry, MochatBufferedEntry,
MoltchatChannel, MochatChannel,
build_buffered_body, build_buffered_body,
resolve_moltchat_target, resolve_mochat_target,
resolve_require_mention, resolve_require_mention,
resolve_was_mentioned, resolve_was_mentioned,
) )
from nanobot.config.schema import MoltchatConfig, MoltchatGroupRule, MoltchatMentionConfig from nanobot.config.schema import MochatConfig, MochatGroupRule, MochatMentionConfig
def test_resolve_moltchat_target_prefixes() -> None: def test_resolve_mochat_target_prefixes() -> None:
t = resolve_moltchat_target("panel:abc") t = resolve_mochat_target("panel:abc")
assert t.id == "abc" assert t.id == "abc"
assert t.is_panel is True assert t.is_panel is True
t = resolve_moltchat_target("session_123") t = resolve_mochat_target("session_123")
assert t.id == "session_123" assert t.id == "session_123"
assert t.is_panel is False assert t.is_panel is False
t = resolve_moltchat_target("mochat:session_456") t = resolve_mochat_target("mochat:session_456")
assert t.id == "session_456" assert t.id == "session_456"
assert t.is_panel is False assert t.is_panel is False
@ -40,12 +40,12 @@ def test_resolve_was_mentioned_from_meta_and_text() -> None:
def test_resolve_require_mention_priority() -> None: def test_resolve_require_mention_priority() -> None:
cfg = MoltchatConfig( cfg = MochatConfig(
groups={ groups={
"*": MoltchatGroupRule(require_mention=False), "*": MochatGroupRule(require_mention=False),
"group-a": MoltchatGroupRule(require_mention=True), "group-a": MochatGroupRule(require_mention=True),
}, },
mention=MoltchatMentionConfig(require_in_groups=False), mention=MochatMentionConfig(require_in_groups=False),
) )
assert resolve_require_mention(cfg, session_id="panel-x", group_id="group-a") is True assert resolve_require_mention(cfg, session_id="panel-x", group_id="group-a") is True
@ -55,14 +55,14 @@ def test_resolve_require_mention_priority() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_delay_buffer_flushes_on_mention() -> None: async def test_delay_buffer_flushes_on_mention() -> None:
bus = MessageBus() bus = MessageBus()
cfg = MoltchatConfig( cfg = MochatConfig(
enabled=True, enabled=True,
claw_token="token", claw_token="token",
agent_user_id="bot", agent_user_id="bot",
reply_delay_mode="non-mention", reply_delay_mode="non-mention",
reply_delay_ms=60_000, reply_delay_ms=60_000,
) )
channel = MoltchatChannel(cfg, bus) channel = MochatChannel(cfg, bus)
first = { first = {
"type": "message.add", "type": "message.add",
@ -94,7 +94,7 @@ async def test_delay_buffer_flushes_on_mention() -> None:
assert bus.inbound_size == 1 assert bus.inbound_size == 1
msg = await bus.consume_inbound() msg = await bus.consume_inbound()
assert msg.channel == "moltchat" assert msg.channel == "mochat"
assert msg.chat_id == "panel-1" assert msg.chat_id == "panel-1"
assert "user1: first" in msg.content assert "user1: first" in msg.content
assert "user2: hello <@bot>" in msg.content assert "user2: hello <@bot>" in msg.content
@ -106,8 +106,8 @@ async def test_delay_buffer_flushes_on_mention() -> None:
def test_build_buffered_body_group_labels() -> None: def test_build_buffered_body_group_labels() -> None:
body = build_buffered_body( body = build_buffered_body(
entries=[ entries=[
MoltchatBufferedEntry(raw_body="a", author="u1", sender_name="Alice"), MochatBufferedEntry(raw_body="a", author="u1", sender_name="Alice"),
MoltchatBufferedEntry(raw_body="b", author="u2", sender_username="bot"), MochatBufferedEntry(raw_body="b", author="u2", sender_username="bot"),
], ],
is_group=True, is_group=True,
) )