refactor(trigger): name CLI trigger source as local

maintainer edit: cron is also a trigger source, so keep the new CLI-delivered source explicitly named as local trigger across backend, WebUI, docs, and tests.
This commit is contained in:
chengyongru
2026-07-02 13:32:46 +08:00
committed by Xubin Ren
parent 1ed2c9a213
commit 2ebf5c4972
40 changed files with 353 additions and 253 deletions
+6 -6
View File
@@ -1,16 +1,16 @@
"""Local external trigger support."""
"""Local trigger support."""
from nanobot.triggers.store import (
ExternalTriggerStore,
from nanobot.triggers.local_store import (
LocalTriggerStore,
TriggerDisabledError,
TriggerNotFoundError,
TriggerStoreError,
)
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery, TriggerRunRecord
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery, TriggerRunRecord
__all__ = [
"ExternalTrigger",
"ExternalTriggerStore",
"LocalTrigger",
"LocalTriggerStore",
"TriggerDelivery",
"TriggerDisabledError",
"TriggerNotFoundError",
@@ -1,4 +1,4 @@
"""Gateway delivery loop for local external triggers."""
"""Gateway delivery loop for local triggers."""
from __future__ import annotations
@@ -10,21 +10,21 @@ from loguru import logger
from nanobot.bus.events import InboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.triggers.session_turns import EXTERNAL_TRIGGER_META
from nanobot.triggers.store import ExternalTriggerStore
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery
from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_META
from nanobot.triggers.local_store import LocalTriggerStore
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
async def run_external_trigger_queue(
async def run_local_trigger_queue(
*,
store: ExternalTriggerStore,
store: LocalTriggerStore,
bus: MessageBus,
poll_interval_s: float = 0.5,
batch_size: int = 20,
) -> None:
"""Poll local trigger deliveries and publish them as normal inbound messages."""
logger.info("External trigger queue started")
logger.info("Local trigger queue started")
recovered = store.recover_processing_deliveries()
if recovered:
logger.warning(
@@ -80,7 +80,7 @@ class _TerminalDeliveryError(RuntimeError):
async def _publish_delivery(
store: ExternalTriggerStore,
store: LocalTriggerStore,
bus: MessageBus,
delivery: TriggerDelivery,
) -> None:
@@ -107,9 +107,9 @@ async def _publish_delivery(
)
def _delivery_metadata(trigger: ExternalTrigger, delivery: TriggerDelivery) -> dict[str, Any]:
def _delivery_metadata(trigger: LocalTrigger, delivery: TriggerDelivery) -> dict[str, Any]:
metadata = dict(trigger.origin_metadata or {})
metadata[EXTERNAL_TRIGGER_META] = {
metadata[LOCAL_TRIGGER_META] = {
"trigger_id": trigger.id,
"trigger_name": trigger.name,
"delivery_id": delivery.id,
@@ -118,7 +118,7 @@ def _delivery_metadata(trigger: ExternalTrigger, delivery: TriggerDelivery) -> d
if trigger.channel == "websocket":
metadata.pop(WEBUI_TURN_METADATA_KEY, None)
metadata[WEBUI_TURN_METADATA_KEY] = f"trigger:{trigger.id}:{uuid.uuid4().hex}"
source: dict[str, str] = {"kind": "trigger"}
source: dict[str, str] = {"kind": "local_trigger"}
if trigger.name:
source["label"] = trigger.name
metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] = source
+51
View File
@@ -0,0 +1,51 @@
"""Shared metadata helpers for local trigger session turns."""
from __future__ import annotations
from typing import Any, Mapping
from nanobot.session.automation_turns import (
AutomationTurnSpec,
automation_history_overrides_for_spec,
automation_trigger,
)
LOCAL_TRIGGER_META = "_local_trigger"
def _local_trigger_history_text(trigger: Mapping[str, Any]) -> str:
name = trigger.get("trigger_name")
trigger_id = trigger.get("trigger_id")
label = name if isinstance(name, str) and name.strip() else trigger_id
return (
f"Local trigger received: {label}"
if isinstance(label, str) and label.strip()
else "Local trigger received"
)
LOCAL_TRIGGER_AUTOMATION_SPEC = AutomationTurnSpec(
kind="local_trigger",
trigger_meta_key=LOCAL_TRIGGER_META,
history_fields={
"trigger_id": "trigger_id",
"trigger_name": "trigger_name",
"trigger_delivery_id": "delivery_id",
},
text_builder=_local_trigger_history_text,
)
def local_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
"""Return structured local trigger metadata when present."""
return automation_trigger(metadata, LOCAL_TRIGGER_AUTOMATION_SPEC)
def local_trigger_history_overrides(
metadata: Mapping[str, Any] | None,
) -> tuple[str | None, dict[str, Any]]:
"""Return session-history text/metadata overrides for a local trigger turn."""
return automation_history_overrides_for_spec(
metadata,
LOCAL_TRIGGER_AUTOMATION_SPEC,
)
@@ -14,7 +14,7 @@ from typing import Any
from filelock import FileLock
from loguru import logger
from nanobot.triggers.types import ExternalTrigger, TriggerDelivery, TriggerRunRecord
from nanobot.triggers.local_types import LocalTrigger, TriggerDelivery, TriggerRunRecord
_TRIGGER_ID_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
_MAX_RUN_HISTORY = 20
@@ -34,7 +34,7 @@ class TriggerDisabledError(TriggerStoreError):
"""Raised when a trigger is disabled."""
class ExternalTriggerStore:
class LocalTriggerStore:
"""Persistent local triggers for one workspace."""
def __init__(self, workspace_path: Path):
@@ -55,8 +55,8 @@ class ExternalTriggerStore:
session_key: str,
sender_id: str = "trigger",
origin_metadata: dict[str, Any] | None = None,
) -> ExternalTrigger:
"""Create a new session-bound external trigger."""
) -> LocalTrigger:
"""Create a new session-bound local trigger."""
clean_name = _clean_name(name)
channel = channel.strip()
chat_id = chat_id.strip()
@@ -70,7 +70,7 @@ class ExternalTriggerStore:
triggers = self._load_triggers_unlocked()
existing_ids = {trigger.id for trigger in triggers}
trigger_id = _new_trigger_id(existing_ids)
trigger = ExternalTrigger(
trigger = LocalTrigger(
id=trigger_id,
name=clean_name,
enabled=True,
@@ -86,7 +86,7 @@ class ExternalTriggerStore:
self._save_triggers_unlocked(triggers)
return trigger
def list_triggers(self, *, include_disabled: bool = False) -> list[ExternalTrigger]:
def list_triggers(self, *, include_disabled: bool = False) -> list[LocalTrigger]:
"""List triggers in this workspace."""
self._ensure_dirs()
with self._lock:
@@ -100,7 +100,7 @@ class ExternalTriggerStore:
session_key: str,
*,
include_disabled: bool = True,
) -> list[ExternalTrigger]:
) -> list[LocalTrigger]:
"""List triggers bound to one session key."""
return [
trigger
@@ -108,13 +108,13 @@ class ExternalTriggerStore:
if trigger.session_key == session_key
]
def get(self, trigger_id: str) -> ExternalTrigger | None:
def get(self, trigger_id: str) -> LocalTrigger | None:
"""Return one trigger by ID."""
self._ensure_dirs()
with self._lock:
return self._find_unlocked(self._load_triggers_unlocked(), trigger_id)
def enable(self, trigger_id: str, *, enabled: bool) -> ExternalTrigger | None:
def enable(self, trigger_id: str, *, enabled: bool) -> LocalTrigger | None:
"""Enable or disable a trigger."""
self._ensure_dirs()
with self._lock:
@@ -127,7 +127,7 @@ class ExternalTriggerStore:
self._save_triggers_unlocked(triggers)
return trigger
def update(self, trigger_id: str, *, name: str | None = None) -> ExternalTrigger | None:
def update(self, trigger_id: str, *, name: str | None = None) -> LocalTrigger | None:
"""Update mutable trigger fields."""
self._ensure_dirs()
with self._lock:
@@ -267,13 +267,13 @@ class ExternalTriggerStore:
self.processing_dir.mkdir(parents=True, exist_ok=True)
self.failed_dir.mkdir(parents=True, exist_ok=True)
def _load_triggers_unlocked(self) -> list[ExternalTrigger]:
def _load_triggers_unlocked(self) -> list[LocalTrigger]:
if not self.store_path.exists():
return []
try:
data = json.loads(self.store_path.read_text(encoding="utf-8"))
return [
ExternalTrigger.from_dict(raw)
LocalTrigger.from_dict(raw)
for raw in data.get("triggers", [])
if isinstance(raw, dict)
]
@@ -288,7 +288,7 @@ class ExternalTriggerStore:
"as a .corrupt-<ts> backup"
) from exc
def _save_triggers_unlocked(self, triggers: list[ExternalTrigger]) -> None:
def _save_triggers_unlocked(self, triggers: list[LocalTrigger]) -> None:
payload = {
"version": 1,
"triggers": [trigger.to_dict() for trigger in triggers],
@@ -297,9 +297,9 @@ class ExternalTriggerStore:
@staticmethod
def _find_unlocked(
triggers: list[ExternalTrigger],
triggers: list[LocalTrigger],
trigger_id: str,
) -> ExternalTrigger | None:
) -> LocalTrigger | None:
return next((trigger for trigger in triggers if trigger.id == trigger_id), None)
def _move_bad_delivery_unlocked(self, path: Path) -> None:
@@ -356,7 +356,7 @@ def _new_trigger_id(existing_ids: set[str]) -> str:
def _clean_name(name: str) -> str:
stripped = " ".join(name.strip().split())
return (stripped or "External trigger")[:120]
return (stripped or "Local trigger")[:120]
def _now_ms() -> int:
@@ -1,4 +1,4 @@
"""Persistent types for local external triggers."""
"""Persistent types for local triggers."""
from __future__ import annotations
@@ -40,7 +40,7 @@ class TriggerRunRecord:
@dataclass
class ExternalTrigger:
class LocalTrigger:
"""A session-bound local trigger."""
id: str
@@ -59,7 +59,7 @@ class ExternalTrigger:
run_history: list[TriggerRunRecord] = field(default_factory=list)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "ExternalTrigger":
def from_dict(cls, data: dict[str, Any]) -> "LocalTrigger":
history = [
record if isinstance(record, TriggerRunRecord) else TriggerRunRecord.from_dict(record)
for record in data.get("runHistory", data.get("run_history", []))
-51
View File
@@ -1,51 +0,0 @@
"""Shared metadata helpers for local external trigger session turns."""
from __future__ import annotations
from typing import Any, Mapping
from nanobot.session.automation_turns import (
AutomationTurnSpec,
automation_history_overrides_for_spec,
automation_trigger,
)
EXTERNAL_TRIGGER_META = "_external_trigger"
def _external_trigger_history_text(trigger: Mapping[str, Any]) -> str:
name = trigger.get("trigger_name")
trigger_id = trigger.get("trigger_id")
label = name if isinstance(name, str) and name.strip() else trigger_id
return (
f"External trigger received: {label}"
if isinstance(label, str) and label.strip()
else "External trigger received"
)
EXTERNAL_TRIGGER_AUTOMATION_SPEC = AutomationTurnSpec(
kind="trigger",
trigger_meta_key=EXTERNAL_TRIGGER_META,
history_fields={
"trigger_id": "trigger_id",
"trigger_name": "trigger_name",
"trigger_delivery_id": "delivery_id",
},
text_builder=_external_trigger_history_text,
)
def external_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
"""Return structured external trigger metadata when present."""
return automation_trigger(metadata, EXTERNAL_TRIGGER_AUTOMATION_SPEC)
def external_trigger_history_overrides(
metadata: Mapping[str, Any] | None,
) -> tuple[str | None, dict[str, Any]]:
"""Return session-history text/metadata overrides for an external trigger turn."""
return automation_history_overrides_for_spec(
metadata,
EXTERNAL_TRIGGER_AUTOMATION_SPEC,
)