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:
+1
-1
@@ -51,7 +51,7 @@ If a local `nanobot agent` session can already answer normally, you can also ask
|
||||
|---|---|---|
|
||||
| Open the bundled browser UI | [`webui.md`](./webui.md) | WebUI on port `8765`, chat workspace, Apps, Skills, Automations, and settings |
|
||||
| Connect Telegram, Discord, WeChat, Slack, and other apps | [`chat-apps.md`](./chat-apps.md) | A gateway-backed chat channel with access control |
|
||||
| Use slash commands and automations | [`chat-commands.md`](./chat-commands.md) | Pairing, model presets, external triggers, heartbeat tasks, and chat-side controls |
|
||||
| Use slash commands and automations | [`chat-commands.md`](./chat-commands.md) | Pairing, model presets, local triggers, heartbeat tasks, and chat-side controls |
|
||||
| Generate images | [`image-generation.md`](./image-generation.md) | Image provider config, WebUI image mode, and artifact behavior |
|
||||
| Run several isolated bots | [`multiple-instances.md`](./multiple-instances.md) | Separate configs, workspaces, ports, and sessions |
|
||||
| Deploy outside a terminal | [`deployment.md`](./deployment.md) | Docker, systemd user services, and macOS LaunchAgent setup |
|
||||
|
||||
@@ -16,8 +16,8 @@ These commands work inside chat channels and interactive agent sessions:
|
||||
| `/dream-restore` | List recent Dream memory versions |
|
||||
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
||||
| `/skill` | List enabled skills and their descriptions |
|
||||
| `/trigger` | Show external trigger usage |
|
||||
| `/trigger <name>` | Create a named local external trigger for the current chat/session |
|
||||
| `/trigger` | Show local trigger usage |
|
||||
| `/trigger <name>` | Create a named local trigger for the current chat/session |
|
||||
| `/pairing` | List pending pairing requests |
|
||||
| `/pairing approve <code>` | Approve a pairing code |
|
||||
| `/pairing deny <code>` | Deny a pending pairing request |
|
||||
@@ -57,7 +57,7 @@ To switch presets for future turns:
|
||||
|
||||
Preset names come from the top-level `modelPresets` config. Switching is runtime-only: it does not rewrite `config.json`, and an in-progress turn keeps using the model it started with. See [Configuration: Model presets](./configuration.md#model-presets) for setup details.
|
||||
|
||||
## External Triggers
|
||||
## Local triggers
|
||||
|
||||
Use `/trigger <name>` when a local script or another service should be able to
|
||||
send a message into the current chat/session later. A name is required; plain
|
||||
|
||||
@@ -13,7 +13,7 @@ Use this page when you know what you want to run and need the command shape. For
|
||||
| Send one test message | `nanobot agent -m "Hello!"` | First proof that install, config, provider, model, and workspace all work |
|
||||
| Chat in the terminal | `nanobot agent` | Interactive local chat; exit with `exit`, `/exit`, `:q`, or `Ctrl+D` |
|
||||
| Use WebUI or chat apps | `nanobot gateway` | Keep this terminal running, or use `nanobot gateway --background` |
|
||||
| Deliver a local external trigger | `nanobot trigger <id> "message"` | Created first with `/trigger <name>` in the target chat/session |
|
||||
| Deliver a local trigger | `nanobot trigger <id> "message"` | Created first with `/trigger <name>` in the target chat/session |
|
||||
| Serve an OpenAI-compatible API | `nanobot serve` | Starts `/v1/chat/completions`, `/v1/models`, and `/health` |
|
||||
| Check chat channel setup | `nanobot channels status` | Useful before starting `nanobot gateway` |
|
||||
| Log in to QR/OAuth-style channels | `nanobot channels login <channel>` | Used by channels such as WhatsApp and WeChat |
|
||||
|
||||
+2
-2
@@ -123,7 +123,7 @@ Tools are discovered automatically from built-in modules and plugin entry points
|
||||
- shell execution with configurable sandboxing;
|
||||
- web search and web fetch with SSRF checks;
|
||||
- MCP servers;
|
||||
- cron reminders, external triggers, and heartbeat tasks;
|
||||
- cron reminders, local triggers, and heartbeat tasks;
|
||||
- image generation;
|
||||
- subagents and runtime self-inspection.
|
||||
|
||||
@@ -143,7 +143,7 @@ User-created reminders use the same cron service but are not the same as the
|
||||
protected heartbeat system job. They run as scheduled turns in their origin
|
||||
chat/session and normally deliver the result back to that channel.
|
||||
|
||||
External triggers are also session-bound, but they do not have their own
|
||||
Local triggers are also session-bound, but they do not have their own
|
||||
schedule. Create one from the target chat with `/trigger <name>`, then call
|
||||
`nanobot trigger <id> "<message>"` when a local script or external service wants
|
||||
nanobot to respond in that session. Webhook servers, third-party auth, and
|
||||
|
||||
+4
-4
@@ -56,7 +56,7 @@ Enter `tokenIssueSecret` when the WebUI asks for a password.
|
||||
| Composer | Send text, images, voice input, slash commands, and `@` mentions for Apps or MCP presets |
|
||||
| Apps | Install, test, update, and use local CLI App adapters and MCP presets |
|
||||
| Skills | Inspect available built-in and workspace skills before relying on them |
|
||||
| Automations | Review, search, run, pause, edit, and delete scheduled and external-trigger agent turns |
|
||||
| Automations | Review, search, run, pause, edit, and delete scheduled and local-trigger agent turns |
|
||||
| Settings | Adjust models, providers, image generation, voice, web tools, runtime, and safety options |
|
||||
|
||||
## Chat Workspace
|
||||
@@ -125,7 +125,7 @@ There are two user-facing automation types:
|
||||
|
||||
- Scheduled automations, created by the agent's cron tool, run at a time,
|
||||
interval, or cron expression.
|
||||
- External triggers, created with `/trigger <name>`, run when you call a local
|
||||
- Local triggers, created with `/trigger <name>`, run when you call a local
|
||||
command such as `nanobot trigger trg_8K4P2Q9X "Review PR #4502"`.
|
||||
|
||||
If a GitHub webhook, CI system, or another service should wake nanobot up, keep
|
||||
@@ -148,7 +148,7 @@ Use the Automations view to:
|
||||
- Sort by next run, last run, updated time, or name.
|
||||
- Run scheduled automations now.
|
||||
- Pause or resume, rename, or delete user-created automations.
|
||||
- Copy the CLI command for external triggers.
|
||||
- Copy the CLI command for local triggers.
|
||||
- Inspect protected system automations without changing them.
|
||||
|
||||
Search accepts plain text and field filters such as `name:backup`,
|
||||
@@ -159,7 +159,7 @@ An automation without a linked chat cannot be enabled or run from the WebUI,
|
||||
because nanobot would not know where to deliver the scheduled turn. Recreate it
|
||||
from the target chat or channel so the automation has complete context.
|
||||
|
||||
External triggers do not have a WebUI "Run now" action because each run needs a
|
||||
Local triggers do not have a WebUI "Run now" action because each run needs a
|
||||
message. Use the copied `nanobot trigger ...` command and replace `"message"`
|
||||
with the content that should be delivered.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class ChannelManager:
|
||||
*,
|
||||
session_manager: "SessionManager | None" = None,
|
||||
cron_service: Any | None = None,
|
||||
external_trigger_store: Any | None = None,
|
||||
local_trigger_store: Any | None = None,
|
||||
webui_runtime_model_name: Callable[[], str | None] | None = None,
|
||||
webui_cron_pending_job_ids: Callable[[str], set[str]] | None = None,
|
||||
webui_static_dist: bool = True,
|
||||
@@ -79,7 +79,7 @@ class ChannelManager:
|
||||
self.bus = bus
|
||||
self._session_manager = session_manager
|
||||
self._cron_service = cron_service
|
||||
self._external_trigger_store = external_trigger_store
|
||||
self._local_trigger_store = local_trigger_store
|
||||
self._webui_runtime_model_name = webui_runtime_model_name
|
||||
self._webui_cron_pending_job_ids = webui_cron_pending_job_ids
|
||||
self._webui_static_dist = webui_static_dist
|
||||
@@ -141,7 +141,7 @@ class ChannelManager:
|
||||
runtime_surface=self._webui_runtime_surface,
|
||||
runtime_capabilities_overrides=self._webui_runtime_capabilities,
|
||||
cron_service=self._cron_service,
|
||||
external_trigger_store=self._external_trigger_store,
|
||||
local_trigger_store=self._local_trigger_store,
|
||||
cron_pending_job_ids=self._webui_cron_pending_job_ids,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
+10
-10
@@ -772,8 +772,8 @@ def trigger(
|
||||
config: str | None = typer.Option(None, "--config", "-c", help="Config file path"),
|
||||
):
|
||||
"""Deliver a local trigger message to its bound chat session."""
|
||||
from nanobot.triggers.store import (
|
||||
ExternalTriggerStore,
|
||||
from nanobot.triggers.local_store import (
|
||||
LocalTriggerStore,
|
||||
TriggerDisabledError,
|
||||
TriggerNotFoundError,
|
||||
TriggerStoreError,
|
||||
@@ -781,7 +781,7 @@ def trigger(
|
||||
|
||||
runtime_config = _load_runtime_config(config, workspace)
|
||||
content = _read_trigger_cli_message(message)
|
||||
store = ExternalTriggerStore(runtime_config.workspace_path)
|
||||
store = LocalTriggerStore(runtime_config.workspace_path)
|
||||
try:
|
||||
delivery = store.enqueue(trigger_id, content)
|
||||
except (TriggerNotFoundError, TriggerDisabledError) as exc:
|
||||
@@ -909,8 +909,8 @@ def _run_gateway(
|
||||
from nanobot.providers.image_generation import image_gen_provider_configs
|
||||
from nanobot.session.manager import SessionManager
|
||||
from nanobot.session.webui_turns import WebuiTurnCoordinator
|
||||
from nanobot.triggers.runner import run_external_trigger_queue
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_runner import run_local_trigger_queue
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
from nanobot.webui.token_usage import TokenUsageHook
|
||||
|
||||
port = port if port is not None else config.gateway.port
|
||||
@@ -933,7 +933,7 @@ def _run_gateway(
|
||||
# Create cron service with workspace-scoped store
|
||||
cron_store_path = config.workspace_path / "cron" / "jobs.json"
|
||||
cron = CronService(cron_store_path)
|
||||
trigger_store = ExternalTriggerStore(config.workspace_path)
|
||||
trigger_store = LocalTriggerStore(config.workspace_path)
|
||||
|
||||
# Create agent with cron service
|
||||
agent = AgentLoop.from_config(
|
||||
@@ -954,7 +954,7 @@ def _run_gateway(
|
||||
sessions=session_manager,
|
||||
schedule_background=lambda coro: agent._schedule_background(coro),
|
||||
).subscribe(runtime_events)
|
||||
agent.external_trigger_store = trigger_store
|
||||
agent.local_trigger_store = trigger_store
|
||||
|
||||
from nanobot.bus.events import OutboundMessage
|
||||
from nanobot.session.keys import session_key_for_channel
|
||||
@@ -1151,7 +1151,7 @@ def _run_gateway(
|
||||
bus,
|
||||
session_manager=session_manager,
|
||||
cron_service=cron,
|
||||
external_trigger_store=trigger_store,
|
||||
local_trigger_store=trigger_store,
|
||||
webui_runtime_model_name=_webui_runtime_model_name,
|
||||
webui_cron_pending_job_ids=getattr(agent, "pending_cron_job_ids_for_session", None),
|
||||
webui_static_dist=webui_static_dist,
|
||||
@@ -1295,8 +1295,8 @@ def _run_gateway(
|
||||
asyncio.create_task(agent.run(), name="nanobot-agent-loop"),
|
||||
asyncio.create_task(channels.start_all(), name="nanobot-channels"),
|
||||
asyncio.create_task(
|
||||
run_external_trigger_queue(store=trigger_store, bus=bus),
|
||||
name="nanobot-external-triggers",
|
||||
run_local_trigger_queue(store=trigger_store, bus=bus),
|
||||
name="nanobot-local-triggers",
|
||||
),
|
||||
]
|
||||
if health_server_enabled:
|
||||
|
||||
@@ -740,7 +740,7 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
||||
metadata={**dict(ctx.msg.metadata or {}), "render_as": "text"},
|
||||
)
|
||||
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
|
||||
loop = ctx.loop
|
||||
workspace = getattr(loop, "workspace", None)
|
||||
@@ -749,9 +749,9 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
||||
if workspace is None:
|
||||
raise RuntimeError("workspace unavailable for trigger creation")
|
||||
|
||||
store = getattr(loop, "external_trigger_store", None)
|
||||
store = getattr(loop, "local_trigger_store", None)
|
||||
if store is None:
|
||||
store = ExternalTriggerStore(workspace)
|
||||
store = LocalTriggerStore(workspace)
|
||||
|
||||
trigger = store.create(
|
||||
name=name,
|
||||
|
||||
@@ -56,9 +56,9 @@ def automation_history_overrides_for_spec(
|
||||
def _automation_specs() -> tuple[AutomationTurnSpec, ...]:
|
||||
# Source modules import the generic helpers above, so keep spec loading lazy.
|
||||
from nanobot.cron.session_turns import CRON_AUTOMATION_SPEC
|
||||
from nanobot.triggers.session_turns import EXTERNAL_TRIGGER_AUTOMATION_SPEC
|
||||
from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_AUTOMATION_SPEC
|
||||
|
||||
return (CRON_AUTOMATION_SPEC, EXTERNAL_TRIGGER_AUTOMATION_SPEC)
|
||||
return (CRON_AUTOMATION_SPEC, LOCAL_TRIGGER_AUTOMATION_SPEC)
|
||||
|
||||
|
||||
def automation_history_overrides(
|
||||
@@ -87,4 +87,6 @@ def is_automation_history_message(message: Mapping[str, Any] | None) -> bool:
|
||||
|
||||
|
||||
def is_automation_kind(value: Any) -> bool:
|
||||
return isinstance(value, str) and any(spec.kind == value for spec in _automation_specs())
|
||||
return isinstance(value, str) and (
|
||||
value == "trigger" or any(spec.kind == value for spec in _automation_specs())
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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", []))
|
||||
@@ -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,
|
||||
)
|
||||
@@ -26,7 +26,7 @@ class GatewayServices:
|
||||
workspaces: WebUIWorkspaceController
|
||||
session_manager: Any | None
|
||||
cron_service: Any | None
|
||||
external_trigger_store: Any | None
|
||||
local_trigger_store: Any | None
|
||||
cron_pending_job_ids: Callable[[str], set[str]] | None
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def build_gateway_services(
|
||||
runtime_capabilities_overrides: dict[str, Any] | None,
|
||||
disabled_skills: set[str] | None = None,
|
||||
cron_service: Any | None = None,
|
||||
external_trigger_store: Any | None = None,
|
||||
local_trigger_store: Any | None = None,
|
||||
cron_pending_job_ids: Callable[[str], set[str]] | None = None,
|
||||
logger: Any = default_logger,
|
||||
) -> GatewayServices:
|
||||
@@ -72,7 +72,7 @@ def build_gateway_services(
|
||||
skills_workspace_path=workspace_path,
|
||||
disabled_skills=disabled_skills,
|
||||
cron_service=cron_service,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
cron_pending_job_ids=cron_pending_job_ids,
|
||||
log=logger,
|
||||
)
|
||||
@@ -84,6 +84,6 @@ def build_gateway_services(
|
||||
workspaces=workspaces,
|
||||
session_manager=session_manager,
|
||||
cron_service=cron_service,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
cron_pending_job_ids=cron_pending_job_ids,
|
||||
)
|
||||
|
||||
@@ -8,9 +8,9 @@ from typing import Any, Protocol
|
||||
from nanobot.cron.types import CronJob
|
||||
from nanobot.session.automation_turns import is_automation_history_message
|
||||
from nanobot.session.manager import _message_preview_text
|
||||
from nanobot.triggers.types import ExternalTrigger
|
||||
from nanobot.triggers.local_types import LocalTrigger
|
||||
|
||||
AutomationJob = CronJob | ExternalTrigger
|
||||
AutomationJob = CronJob | LocalTrigger
|
||||
|
||||
|
||||
class _CronServiceLike(Protocol):
|
||||
@@ -24,15 +24,15 @@ class _CronServiceLike(Protocol):
|
||||
) -> list[CronJob]: ...
|
||||
|
||||
|
||||
class _ExternalTriggerStoreLike(Protocol):
|
||||
def list_triggers(self, *, include_disabled: bool = False) -> list[ExternalTrigger]: ...
|
||||
class _LocalTriggerStoreLike(Protocol):
|
||||
def list_triggers(self, *, include_disabled: bool = False) -> list[LocalTrigger]: ...
|
||||
|
||||
def list_for_session(
|
||||
self,
|
||||
session_key: str,
|
||||
*,
|
||||
include_disabled: bool = True,
|
||||
) -> list[ExternalTrigger]: ...
|
||||
) -> list[LocalTrigger]: ...
|
||||
|
||||
|
||||
class _SessionManagerLike(Protocol):
|
||||
@@ -43,7 +43,7 @@ def session_automation_jobs(
|
||||
cron_service: _CronServiceLike | None,
|
||||
session_key: str,
|
||||
*,
|
||||
external_trigger_store: _ExternalTriggerStoreLike | None = None,
|
||||
local_trigger_store: _LocalTriggerStoreLike | None = None,
|
||||
) -> list[AutomationJob]:
|
||||
"""Return user automations attached to the WebUI session."""
|
||||
jobs: list[AutomationJob] = []
|
||||
@@ -54,9 +54,9 @@ def session_automation_jobs(
|
||||
include_disabled=True,
|
||||
)
|
||||
)
|
||||
if external_trigger_store is not None:
|
||||
if local_trigger_store is not None:
|
||||
jobs.extend(
|
||||
external_trigger_store.list_for_session(
|
||||
local_trigger_store.list_for_session(
|
||||
session_key,
|
||||
include_disabled=True,
|
||||
)
|
||||
@@ -68,7 +68,7 @@ def session_automations_payload(
|
||||
cron_service: _CronServiceLike | None,
|
||||
session_key: str,
|
||||
*,
|
||||
external_trigger_store: _ExternalTriggerStoreLike | None = None,
|
||||
local_trigger_store: _LocalTriggerStoreLike | None = None,
|
||||
pending_job_ids: Collection[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return user-created automation jobs attached to a WebUI session."""
|
||||
@@ -77,7 +77,7 @@ def session_automations_payload(
|
||||
session_automation_jobs(
|
||||
cron_service,
|
||||
session_key,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
),
|
||||
pending_job_ids=pending_job_ids,
|
||||
)
|
||||
@@ -87,7 +87,7 @@ def session_automations_payload(
|
||||
def all_automations_payload(
|
||||
cron_service: _CronServiceLike | None,
|
||||
*,
|
||||
external_trigger_store: _ExternalTriggerStoreLike | None = None,
|
||||
local_trigger_store: _LocalTriggerStoreLike | None = None,
|
||||
session_manager: _SessionManagerLike | None = None,
|
||||
pending_job_ids: Collection[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -95,8 +95,8 @@ def all_automations_payload(
|
||||
jobs: list[AutomationJob] = []
|
||||
if cron_service is not None:
|
||||
jobs.extend(cron_service.list_jobs(include_disabled=True))
|
||||
if external_trigger_store is not None:
|
||||
jobs.extend(external_trigger_store.list_triggers(include_disabled=True))
|
||||
if local_trigger_store is not None:
|
||||
jobs.extend(local_trigger_store.list_triggers(include_disabled=True))
|
||||
return {
|
||||
"jobs": serialize_automation_jobs(
|
||||
jobs,
|
||||
@@ -132,7 +132,7 @@ def _serialize_job(
|
||||
include_details: bool = False,
|
||||
session_manager: _SessionManagerLike | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if isinstance(job, ExternalTrigger):
|
||||
if isinstance(job, LocalTrigger):
|
||||
return _serialize_trigger(
|
||||
job,
|
||||
include_details=include_details,
|
||||
@@ -187,7 +187,7 @@ def _serialize_job(
|
||||
|
||||
|
||||
def _serialize_trigger(
|
||||
trigger: ExternalTrigger,
|
||||
trigger: LocalTrigger,
|
||||
*,
|
||||
include_details: bool = False,
|
||||
session_manager: _SessionManagerLike | None = None,
|
||||
@@ -197,16 +197,16 @@ def _serialize_trigger(
|
||||
"id": trigger.id,
|
||||
"name": trigger.name,
|
||||
"enabled": trigger.enabled,
|
||||
"kind": "external_trigger",
|
||||
"kind": "local_trigger",
|
||||
"schedule": {
|
||||
"kind": "external",
|
||||
"kind": "local",
|
||||
"at_ms": None,
|
||||
"every_ms": None,
|
||||
"expr": None,
|
||||
"tz": None,
|
||||
},
|
||||
"payload": {
|
||||
"kind": "external_trigger",
|
||||
"kind": "local_trigger",
|
||||
"message": command,
|
||||
"command": command,
|
||||
},
|
||||
@@ -273,7 +273,7 @@ def _origin_payload(
|
||||
|
||||
|
||||
def _trigger_origin_payload(
|
||||
trigger: ExternalTrigger,
|
||||
trigger: LocalTrigger,
|
||||
session_manager: _SessionManagerLike | None,
|
||||
) -> dict[str, Any] | None:
|
||||
channel = trigger.channel
|
||||
|
||||
+24
-24
@@ -26,7 +26,7 @@ from websockets.http11 import Response
|
||||
from nanobot.command.builtin import builtin_command_palette
|
||||
from nanobot.cron.session_turns import is_bound_cron_job
|
||||
from nanobot.cron.types import CronJob, CronSchedule
|
||||
from nanobot.triggers.types import ExternalTrigger
|
||||
from nanobot.triggers.local_types import LocalTrigger
|
||||
from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel
|
||||
from nanobot.webui.file_preview import WebUIFilePreviewError, file_preview_payload
|
||||
from nanobot.webui.gateway_tokens import GatewayTokenStore, token_response_payload
|
||||
@@ -90,7 +90,7 @@ if TYPE_CHECKING:
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.cron.service import CronService
|
||||
from nanobot.session.manager import SessionManager
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
|
||||
|
||||
def _decode_api_key(raw_key: str) -> str | None:
|
||||
@@ -155,7 +155,7 @@ class GatewayHTTPHandler:
|
||||
skills_workspace_path: Path,
|
||||
disabled_skills: set[str] | None = None,
|
||||
cron_service: CronService | None = None,
|
||||
external_trigger_store: ExternalTriggerStore | None = None,
|
||||
local_trigger_store: LocalTriggerStore | None = None,
|
||||
cron_pending_job_ids: Callable[[str], set[str]] | None = None,
|
||||
log: Any = logger,
|
||||
) -> None:
|
||||
@@ -170,7 +170,7 @@ class GatewayHTTPHandler:
|
||||
self.skills_workspace_path = skills_workspace_path
|
||||
self.disabled_skills = disabled_skills or set()
|
||||
self.cron_service = cron_service
|
||||
self.external_trigger_store = external_trigger_store
|
||||
self.local_trigger_store = local_trigger_store
|
||||
self.cron_pending_job_ids = cron_pending_job_ids
|
||||
self._log = log
|
||||
self._runtime_surface = runtime_surface
|
||||
@@ -494,7 +494,7 @@ class GatewayHTTPHandler:
|
||||
session_automations_payload(
|
||||
self.cron_service,
|
||||
decoded_key,
|
||||
external_trigger_store=self.external_trigger_store,
|
||||
local_trigger_store=self.local_trigger_store,
|
||||
pending_job_ids=pending_job_ids,
|
||||
)
|
||||
)
|
||||
@@ -514,7 +514,7 @@ class GatewayHTTPHandler:
|
||||
automation_jobs = session_automation_jobs(
|
||||
self.cron_service,
|
||||
decoded_key,
|
||||
external_trigger_store=self.external_trigger_store,
|
||||
local_trigger_store=self.local_trigger_store,
|
||||
)
|
||||
if automation_jobs and delete_automations not in {"1", "true", "yes"}:
|
||||
return _http_json_response(
|
||||
@@ -526,9 +526,9 @@ class GatewayHTTPHandler:
|
||||
)
|
||||
if automation_jobs:
|
||||
for job in automation_jobs:
|
||||
if isinstance(job, ExternalTrigger):
|
||||
if self.external_trigger_store is not None:
|
||||
self.external_trigger_store.delete(job.id)
|
||||
if isinstance(job, LocalTrigger):
|
||||
if self.local_trigger_store is not None:
|
||||
self.local_trigger_store.delete(job.id)
|
||||
elif self.cron_service is not None:
|
||||
self.cron_service.remove_job(job.id)
|
||||
deleted = self.session_manager.delete_session(decoded_key)
|
||||
@@ -567,7 +567,7 @@ class GatewayHTTPHandler:
|
||||
return _http_json_response(
|
||||
all_automations_payload(
|
||||
self.cron_service,
|
||||
external_trigger_store=self.external_trigger_store,
|
||||
local_trigger_store=self.local_trigger_store,
|
||||
session_manager=self.session_manager,
|
||||
pending_job_ids=self._pending_cron_job_ids_for_all(),
|
||||
)
|
||||
@@ -580,16 +580,16 @@ class GatewayHTTPHandler:
|
||||
) -> Response:
|
||||
if not self.check_api_token(request):
|
||||
return _http_error(401, "Unauthorized")
|
||||
if self.cron_service is None and self.external_trigger_store is None:
|
||||
if self.cron_service is None and self.local_trigger_store is None:
|
||||
return _http_error(503, "automation service unavailable")
|
||||
|
||||
query = _parse_query(request.path)
|
||||
job_id = (_query_first(query, "id") or _query_first(query, "job_id") or "").strip()
|
||||
if not job_id:
|
||||
return _http_error(400, "missing automation id")
|
||||
trigger = self.external_trigger_store.get(job_id) if self.external_trigger_store else None
|
||||
trigger = self.local_trigger_store.get(job_id) if self.local_trigger_store else None
|
||||
if trigger is not None:
|
||||
return self._handle_external_trigger_action(request, action, trigger)
|
||||
return self._handle_local_trigger_action(request, action, trigger)
|
||||
|
||||
if self.cron_service is None:
|
||||
return _http_error(404, "automation not found")
|
||||
@@ -638,34 +638,34 @@ class GatewayHTTPHandler:
|
||||
|
||||
return self._handle_webui_automations(request)
|
||||
|
||||
def _handle_external_trigger_action(
|
||||
def _handle_local_trigger_action(
|
||||
self,
|
||||
request: WsRequest,
|
||||
action: str,
|
||||
trigger: ExternalTrigger,
|
||||
trigger: LocalTrigger,
|
||||
) -> Response:
|
||||
if self.external_trigger_store is None:
|
||||
if self.local_trigger_store is None:
|
||||
return _http_error(503, "trigger service unavailable")
|
||||
if action == "enable":
|
||||
if self.external_trigger_store.enable(trigger.id, enabled=True) is None:
|
||||
if self.local_trigger_store.enable(trigger.id, enabled=True) is None:
|
||||
return _http_error(404, "automation not found")
|
||||
elif action == "disable":
|
||||
if self.external_trigger_store.enable(trigger.id, enabled=False) is None:
|
||||
if self.local_trigger_store.enable(trigger.id, enabled=False) is None:
|
||||
return _http_error(404, "automation not found")
|
||||
elif action == "delete":
|
||||
if not self.external_trigger_store.delete(trigger.id):
|
||||
if not self.local_trigger_store.delete(trigger.id):
|
||||
return _http_error(404, "automation not found")
|
||||
elif action == "run":
|
||||
return _http_error(409, "external trigger requires a CLI message")
|
||||
return _http_error(409, "local trigger requires a CLI message")
|
||||
elif action == "update":
|
||||
values = _automation_values_from_request(request)
|
||||
if values is None:
|
||||
return _http_error(400, "invalid automation update payload")
|
||||
parsed = _parse_external_trigger_update(values)
|
||||
parsed = _parse_local_trigger_update(values)
|
||||
if isinstance(parsed, str):
|
||||
return _http_error(400, parsed)
|
||||
if parsed:
|
||||
if self.external_trigger_store.update(trigger.id, **parsed) is None:
|
||||
if self.local_trigger_store.update(trigger.id, **parsed) is None:
|
||||
return _http_error(404, "automation not found")
|
||||
else:
|
||||
return _http_error(404, "unknown automation action")
|
||||
@@ -884,7 +884,7 @@ def _parse_automation_update(
|
||||
return update
|
||||
|
||||
|
||||
def _parse_external_trigger_update(values: dict[str, Any]) -> dict[str, Any] | str:
|
||||
def _parse_local_trigger_update(values: dict[str, Any]) -> dict[str, Any] | str:
|
||||
update: dict[str, Any] = {}
|
||||
if "name" in values:
|
||||
raw_name = values.get("name")
|
||||
@@ -896,7 +896,7 @@ def _parse_external_trigger_update(values: dict[str, Any]) -> dict[str, Any] | s
|
||||
update["name"] = name
|
||||
forbidden = [key for key in ("message", "schedule") if key in values]
|
||||
if forbidden:
|
||||
return "external trigger updates only support name"
|
||||
return "local trigger updates only support name"
|
||||
return update
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from nanobot.session.webui_turns import (
|
||||
clean_generated_title,
|
||||
maybe_generate_webui_title,
|
||||
)
|
||||
from nanobot.triggers.session_turns import EXTERNAL_TRIGGER_META
|
||||
from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_META
|
||||
from nanobot.utils.llm_runtime import LLMRuntime
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ def test_persist_cron_turn_uses_distinct_history_marker(tmp_path: Path) -> None:
|
||||
assert message["cron_prompt_ref"] == prompt_ref
|
||||
|
||||
|
||||
def test_persist_external_trigger_turn_uses_hidden_automation_marker(tmp_path: Path) -> None:
|
||||
def test_persist_local_trigger_turn_uses_hidden_automation_marker(tmp_path: Path) -> None:
|
||||
loop = _make_full_loop(tmp_path)
|
||||
session = loop.sessions.get_or_create("websocket:auto")
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_persist_external_trigger_turn_uses_hidden_automation_marker(tmp_path: P
|
||||
chat_id="auto",
|
||||
content="Review PR #4502",
|
||||
metadata={
|
||||
EXTERNAL_TRIGGER_META: {
|
||||
LOCAL_TRIGGER_META: {
|
||||
"trigger_id": "trg_123",
|
||||
"trigger_name": "PR review",
|
||||
"delivery_id": "tdel_456",
|
||||
@@ -142,15 +142,15 @@ def test_persist_external_trigger_turn_uses_hidden_automation_marker(tmp_path: P
|
||||
|
||||
assert persisted is True
|
||||
message = session.messages[-1]
|
||||
assert message["content"] == "External trigger received: PR review"
|
||||
assert message["content"] == "Local trigger received: PR review"
|
||||
assert "Review PR #4502" not in message["content"]
|
||||
assert message[AUTOMATION_HISTORY_META] == {
|
||||
"kind": "trigger",
|
||||
"kind": "local_trigger",
|
||||
"trigger_id": "trg_123",
|
||||
"trigger_name": "PR review",
|
||||
"trigger_delivery_id": "tdel_456",
|
||||
}
|
||||
assert EXTERNAL_TRIGGER_META not in message
|
||||
assert LOCAL_TRIGGER_META not in message
|
||||
assert message["trigger_id"] == "trg_123"
|
||||
assert message["trigger_name"] == "PR review"
|
||||
assert message["trigger_delivery_id"] == "tdel_456"
|
||||
|
||||
@@ -2996,8 +2996,8 @@ def test_handle_webui_thread_get_does_not_backfill_trigger_internal_prompt(
|
||||
session = sessions.get_or_create(key)
|
||||
session.add_message(
|
||||
"user",
|
||||
"External trigger received: PR review",
|
||||
**{AUTOMATION_HISTORY_META: {"kind": "trigger", "trigger_id": "trg_123"}},
|
||||
"Local trigger received: PR review",
|
||||
**{AUTOMATION_HISTORY_META: {"kind": "local_trigger", "trigger_id": "trg_123"}},
|
||||
)
|
||||
session.add_message("assistant", "PR #4502 已经开始 review。")
|
||||
sessions.save(session)
|
||||
|
||||
@@ -20,7 +20,7 @@ from nanobot.cron.service import CronService
|
||||
from nanobot.cron.types import CronJob, CronPayload, CronSchedule
|
||||
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||
from nanobot.session.manager import Session, SessionManager
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
from nanobot.webui.gateway_services import GatewayServices, build_gateway_services
|
||||
|
||||
_PORT = 29900
|
||||
@@ -47,7 +47,7 @@ def _make_handler(
|
||||
workspace_path: Path | None = None,
|
||||
runtime_model_name: Any | None = None,
|
||||
cron_service: CronService | None = None,
|
||||
external_trigger_store: ExternalTriggerStore | None = None,
|
||||
local_trigger_store: LocalTriggerStore | None = None,
|
||||
cron_pending_job_ids: Any | None = None,
|
||||
) -> GatewayServices:
|
||||
config = WebSocketConfig.model_validate(cfg) if isinstance(cfg, dict) else cfg
|
||||
@@ -63,7 +63,7 @@ def _make_handler(
|
||||
runtime_surface="browser",
|
||||
runtime_capabilities_overrides=None,
|
||||
cron_service=cron_service,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
cron_pending_job_ids=cron_pending_job_ids,
|
||||
)
|
||||
|
||||
@@ -77,7 +77,7 @@ def _ch(
|
||||
port: int = _PORT,
|
||||
runtime_model_name: Any | None = None,
|
||||
cron_service: CronService | None = None,
|
||||
external_trigger_store: ExternalTriggerStore | None = None,
|
||||
local_trigger_store: LocalTriggerStore | None = None,
|
||||
cron_pending_job_ids: Any | None = None,
|
||||
**extra: Any,
|
||||
) -> WebSocketChannel:
|
||||
@@ -97,7 +97,7 @@ def _ch(
|
||||
workspace_path=workspace_path,
|
||||
runtime_model_name=runtime_model_name,
|
||||
cron_service=cron_service,
|
||||
external_trigger_store=external_trigger_store,
|
||||
local_trigger_store=local_trigger_store,
|
||||
cron_pending_job_ids=cron_pending_job_ids,
|
||||
)
|
||||
return WebSocketChannel(cfg, bus, gateway=gateway)
|
||||
@@ -325,12 +325,12 @@ async def test_session_automations_route_ignores_unified_owner(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_automations_route_lists_external_triggers(
|
||||
async def test_session_automations_route_lists_local_triggers(
|
||||
bus: MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
port = _free_port()
|
||||
base_url = f"http://127.0.0.1:{port}"
|
||||
trigger_store = ExternalTriggerStore(tmp_path)
|
||||
trigger_store = LocalTriggerStore(tmp_path)
|
||||
trigger = trigger_store.create(
|
||||
name="PR review",
|
||||
channel="websocket",
|
||||
@@ -340,7 +340,7 @@ async def test_session_automations_route_lists_external_triggers(
|
||||
channel = _ch(
|
||||
bus,
|
||||
session_manager=_seed_session(tmp_path, key="websocket:abc"),
|
||||
external_trigger_store=trigger_store,
|
||||
local_trigger_store=trigger_store,
|
||||
port=port,
|
||||
)
|
||||
server_task = asyncio.create_task(channel.start())
|
||||
@@ -359,9 +359,9 @@ async def test_session_automations_route_lists_external_triggers(
|
||||
body = resp.json()
|
||||
assert [job["id"] for job in body["jobs"]] == [trigger.id]
|
||||
job = body["jobs"][0]
|
||||
assert job["kind"] == "external_trigger"
|
||||
assert job["schedule"]["kind"] == "external"
|
||||
assert job["payload"]["kind"] == "external_trigger"
|
||||
assert job["kind"] == "local_trigger"
|
||||
assert job["schedule"]["kind"] == "local"
|
||||
assert job["payload"]["kind"] == "local_trigger"
|
||||
assert job["payload"]["command"] == f'nanobot trigger {trigger.id} "message"'
|
||||
finally:
|
||||
await channel.stop()
|
||||
@@ -1130,12 +1130,12 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webui_automations_route_manages_external_triggers(
|
||||
async def test_webui_automations_route_manages_local_triggers(
|
||||
bus: MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
port = _free_port()
|
||||
base_url = f"http://127.0.0.1:{port}"
|
||||
trigger_store = ExternalTriggerStore(tmp_path)
|
||||
trigger_store = LocalTriggerStore(tmp_path)
|
||||
trigger = trigger_store.create(
|
||||
name="PR review",
|
||||
channel="websocket",
|
||||
@@ -1145,7 +1145,7 @@ async def test_webui_automations_route_manages_external_triggers(
|
||||
channel = _ch(
|
||||
bus,
|
||||
session_manager=_seed_session(tmp_path, key="websocket:abc"),
|
||||
external_trigger_store=trigger_store,
|
||||
local_trigger_store=trigger_store,
|
||||
port=port,
|
||||
)
|
||||
server_task = asyncio.create_task(channel.start())
|
||||
@@ -1158,7 +1158,7 @@ async def test_webui_automations_route_manages_external_triggers(
|
||||
listed = await _http_get(f"{base_url}/api/webui/automations", headers=auth)
|
||||
assert listed.status_code == 200
|
||||
by_id = {job["id"]: job for job in listed.json()["jobs"]}
|
||||
assert by_id[trigger.id]["kind"] == "external_trigger"
|
||||
assert by_id[trigger.id]["kind"] == "local_trigger"
|
||||
assert by_id[trigger.id]["trigger"]["command"] == f'nanobot trigger {trigger.id} "message"'
|
||||
|
||||
disabled = await _http_get(
|
||||
@@ -1251,14 +1251,14 @@ async def test_session_delete_blocks_when_bound_automation_exists(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_delete_blocks_and_cascades_external_triggers(
|
||||
async def test_session_delete_blocks_and_cascades_local_triggers(
|
||||
bus: MagicMock, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
|
||||
port = _free_port()
|
||||
base_url = f"http://127.0.0.1:{port}"
|
||||
sm = _seed_session(tmp_path, key="websocket:doomed")
|
||||
trigger_store = ExternalTriggerStore(tmp_path)
|
||||
trigger_store = LocalTriggerStore(tmp_path)
|
||||
trigger = trigger_store.create(
|
||||
name="PR review",
|
||||
channel="websocket",
|
||||
@@ -1268,7 +1268,7 @@ async def test_session_delete_blocks_and_cascades_external_triggers(
|
||||
channel = _ch(
|
||||
bus,
|
||||
session_manager=sm,
|
||||
external_trigger_store=trigger_store,
|
||||
local_trigger_store=trigger_store,
|
||||
port=port,
|
||||
)
|
||||
server_task = asyncio.create_task(channel.start())
|
||||
|
||||
@@ -2520,14 +2520,14 @@ def test_trigger_cli_queues_message_in_workspace(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
|
||||
config_file = _write_instance_config(tmp_path)
|
||||
config = Config()
|
||||
config.agents.defaults.workspace = str(tmp_path / "workspace")
|
||||
_patch_cli_command_runtime(monkeypatch, config)
|
||||
|
||||
store = ExternalTriggerStore(config.workspace_path)
|
||||
store = LocalTriggerStore(config.workspace_path)
|
||||
trigger = store.create(
|
||||
name="Review hook",
|
||||
channel="websocket",
|
||||
|
||||
@@ -8,15 +8,15 @@ import pytest
|
||||
from nanobot.bus.events import InboundMessage
|
||||
from nanobot.command.builtin import build_help_text, register_builtin_commands
|
||||
from nanobot.command.router import CommandContext, CommandRouter
|
||||
from nanobot.triggers.store import ExternalTriggerStore
|
||||
from nanobot.triggers.local_store import LocalTriggerStore
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_command_creates_session_bound_local_trigger(tmp_path: Path) -> None:
|
||||
router = CommandRouter()
|
||||
register_builtin_commands(router)
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
loop = SimpleNamespace(workspace=tmp_path, external_trigger_store=store)
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
loop = SimpleNamespace(workspace=tmp_path, local_trigger_store=store)
|
||||
msg = InboundMessage(
|
||||
channel="websocket",
|
||||
sender_id="user",
|
||||
@@ -49,8 +49,8 @@ async def test_trigger_command_creates_session_bound_local_trigger(tmp_path: Pat
|
||||
async def test_trigger_command_without_name_returns_usage_only(tmp_path: Path) -> None:
|
||||
router = CommandRouter()
|
||||
register_builtin_commands(router)
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
loop = SimpleNamespace(workspace=tmp_path, external_trigger_store=store)
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
loop = SimpleNamespace(workspace=tmp_path, local_trigger_store=store)
|
||||
msg = InboundMessage(
|
||||
channel="websocket",
|
||||
sender_id="user",
|
||||
|
||||
@@ -7,13 +7,13 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from nanobot.bus.events import InboundMessage
|
||||
from nanobot.triggers.runner import run_external_trigger_queue
|
||||
from nanobot.triggers.store import ExternalTriggerStore, TriggerDisabledError
|
||||
from nanobot.triggers.local_runner import run_local_trigger_queue
|
||||
from nanobot.triggers.local_store import LocalTriggerStore, TriggerDisabledError
|
||||
from nanobot.webui.metadata import WEBUI_MESSAGE_SOURCE_METADATA_KEY, WEBUI_TURN_METADATA_KEY
|
||||
|
||||
|
||||
def test_trigger_store_allows_multiple_triggers_per_session(tmp_path: Path) -> None:
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
|
||||
first = store.create(
|
||||
name="PR review",
|
||||
@@ -36,7 +36,7 @@ def test_trigger_store_allows_multiple_triggers_per_session(tmp_path: Path) -> N
|
||||
|
||||
|
||||
def test_enqueue_rejects_disabled_trigger(tmp_path: Path) -> None:
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
trigger = store.create(
|
||||
name="Disabled",
|
||||
channel="telegram",
|
||||
@@ -50,7 +50,7 @@ def test_enqueue_rejects_disabled_trigger(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_recover_processing_deliveries_requeues_claimed_delivery(tmp_path: Path) -> None:
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
trigger = store.create(
|
||||
name="PR review",
|
||||
channel="websocket",
|
||||
@@ -63,9 +63,9 @@ def test_recover_processing_deliveries_requeues_claimed_delivery(tmp_path: Path)
|
||||
assert len(claimed) == 1
|
||||
assert claimed[0].path is not None
|
||||
assert claimed[0].path.parent.name == "processing"
|
||||
assert ExternalTriggerStore(tmp_path).claim_deliveries() == []
|
||||
assert LocalTriggerStore(tmp_path).claim_deliveries() == []
|
||||
|
||||
restarted = ExternalTriggerStore(tmp_path)
|
||||
restarted = LocalTriggerStore(tmp_path)
|
||||
assert restarted.recover_processing_deliveries() == 1
|
||||
|
||||
reclaimed = restarted.claim_deliveries()
|
||||
@@ -77,8 +77,8 @@ def test_recover_processing_deliveries_requeues_claimed_delivery(tmp_path: Path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_external_trigger_queue_publishes_bound_inbound_message(tmp_path: Path) -> None:
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
async def test_local_trigger_queue_publishes_bound_inbound_message(tmp_path: Path) -> None:
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
trigger = store.create(
|
||||
name="PR review",
|
||||
channel="websocket",
|
||||
@@ -94,7 +94,7 @@ async def test_external_trigger_queue_publishes_bound_inbound_message(tmp_path:
|
||||
published.append(msg)
|
||||
|
||||
task = asyncio.create_task(
|
||||
run_external_trigger_queue(store=store, bus=_Bus(), poll_interval_s=0.01)
|
||||
run_local_trigger_queue(store=store, bus=_Bus(), poll_interval_s=0.01)
|
||||
)
|
||||
try:
|
||||
for _ in range(100):
|
||||
@@ -116,10 +116,10 @@ async def test_external_trigger_queue_publishes_bound_inbound_message(tmp_path:
|
||||
assert msg.metadata[WEBUI_TURN_METADATA_KEY].startswith(f"trigger:{trigger.id}:")
|
||||
assert msg.metadata[WEBUI_TURN_METADATA_KEY] != "old-turn"
|
||||
assert msg.metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] == {
|
||||
"kind": "trigger",
|
||||
"kind": "local_trigger",
|
||||
"label": "PR review",
|
||||
}
|
||||
assert msg.metadata["_external_trigger"]["trigger_id"] == trigger.id
|
||||
assert msg.metadata["_local_trigger"]["trigger_id"] == trigger.id
|
||||
|
||||
stored = store.get(trigger.id)
|
||||
assert stored is not None
|
||||
@@ -129,10 +129,10 @@ async def test_external_trigger_queue_publishes_bound_inbound_message(tmp_path:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_external_trigger_queue_recovers_processing_delivery_on_start(
|
||||
async def test_local_trigger_queue_recovers_processing_delivery_on_start(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = ExternalTriggerStore(tmp_path)
|
||||
store = LocalTriggerStore(tmp_path)
|
||||
trigger = store.create(
|
||||
name="PR review",
|
||||
channel="websocket",
|
||||
@@ -147,9 +147,9 @@ async def test_external_trigger_queue_recovers_processing_delivery_on_start(
|
||||
async def publish_inbound(self, msg: InboundMessage) -> None:
|
||||
published.append(msg)
|
||||
|
||||
restarted = ExternalTriggerStore(tmp_path)
|
||||
restarted = LocalTriggerStore(tmp_path)
|
||||
task = asyncio.create_task(
|
||||
run_external_trigger_queue(store=restarted, bus=_Bus(), poll_interval_s=0.01)
|
||||
run_local_trigger_queue(store=restarted, bus=_Bus(), poll_interval_s=0.01)
|
||||
)
|
||||
try:
|
||||
for _ in range(100):
|
||||
@@ -163,5 +163,5 @@ async def test_external_trigger_queue_recovers_processing_delivery_on_start(
|
||||
|
||||
assert len(published) == 1
|
||||
assert published[0].content == "Review PR #4591"
|
||||
assert published[0].metadata["_external_trigger"]["trigger_id"] == trigger.id
|
||||
assert published[0].metadata["_local_trigger"]["trigger_id"] == trigger.id
|
||||
assert restarted.claim_deliveries() == []
|
||||
@@ -473,7 +473,25 @@ def test_replay_reused_turn_id_after_turn_end_starts_new_turn(tmp_path, monkeypa
|
||||
assert msgs[2]["source"] == {"kind": "cron", "label": "drink water"}
|
||||
|
||||
|
||||
def test_replay_preserves_trigger_source_metadata(tmp_path, monkeypatch) -> None:
|
||||
def test_replay_preserves_local_trigger_source_metadata(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
|
||||
key = "websocket:t-local-trigger-source"
|
||||
append_transcript_object(
|
||||
key,
|
||||
{
|
||||
"event": "message",
|
||||
"chat_id": "t-local-trigger-source",
|
||||
"text": "PR #4502 review started.",
|
||||
"source": {"kind": "local_trigger", "label": "PR review"},
|
||||
},
|
||||
)
|
||||
|
||||
msgs = replay_transcript_to_ui_messages(read_transcript_lines(key))
|
||||
|
||||
assert msgs[0]["source"] == {"kind": "local_trigger", "label": "PR review"}
|
||||
|
||||
|
||||
def test_replay_preserves_legacy_trigger_source_metadata(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
|
||||
key = "websocket:t-trigger-source"
|
||||
append_transcript_object(
|
||||
|
||||
@@ -94,8 +94,8 @@ def test_webui_session_list_skips_trigger_internal_user_preview(tmp_path: Path)
|
||||
session = manager.get_or_create("websocket:trigger-preview")
|
||||
session.add_message(
|
||||
"user",
|
||||
"External trigger received: PR review",
|
||||
**{AUTOMATION_HISTORY_META: {"kind": "trigger", "trigger_id": "trg_123"}},
|
||||
"Local trigger received: PR review",
|
||||
**{AUTOMATION_HISTORY_META: {"kind": "local_trigger", "trigger_id": "trg_123"}},
|
||||
)
|
||||
session.add_message("assistant", "PR #4502 已经开始 review。")
|
||||
manager.save(session)
|
||||
|
||||
@@ -122,8 +122,8 @@ function formatAutomationSchedule(
|
||||
})
|
||||
: t("deleteConfirm.schedule.cron", { expr: job.schedule.expr });
|
||||
}
|
||||
if (job.schedule.kind === "external" || job.payload.kind === "external_trigger") {
|
||||
return t("deleteConfirm.schedule.external", { defaultValue: "External trigger" });
|
||||
if (job.schedule.kind === "local" || job.payload.kind === "local_trigger") {
|
||||
return t("deleteConfirm.schedule.local", { defaultValue: "Local trigger" });
|
||||
}
|
||||
return t("deleteConfirm.schedule.unknown");
|
||||
}
|
||||
@@ -134,8 +134,8 @@ function formatAutomationNextRun(
|
||||
locale: string,
|
||||
): string {
|
||||
if (!job.enabled) return t("deleteConfirm.next.disabled");
|
||||
if (job.schedule.kind === "external" || job.payload.kind === "external_trigger") {
|
||||
return t("deleteConfirm.next.external", { defaultValue: "Waiting for trigger" });
|
||||
if (job.schedule.kind === "local" || job.payload.kind === "local_trigger") {
|
||||
return t("deleteConfirm.next.local", { defaultValue: "Waiting for trigger" });
|
||||
}
|
||||
const next = job.state.next_run_at_ms;
|
||||
if (!next) return t("deleteConfirm.next.none");
|
||||
|
||||
@@ -167,7 +167,12 @@ export function MessageBubble({
|
||||
const reasoning = message.role === "assistant" ? message.reasoning ?? "" : "";
|
||||
const reasoningStreaming = !!(message.role === "assistant" && message.reasoningStreaming);
|
||||
const hasReasoning = reasoning.length > 0 || reasoningStreaming;
|
||||
const automationSourceLabel = message.source?.kind === "cron" || message.source?.kind === "trigger"
|
||||
const automationSourceKind = message.source?.kind;
|
||||
const automationSourceLabel = (
|
||||
automationSourceKind === "cron"
|
||||
|| automationSourceKind === "local_trigger"
|
||||
|| automationSourceKind === "trigger"
|
||||
)
|
||||
? (message.source.label?.trim() || t("message.automationSourceFallback"))
|
||||
: "";
|
||||
const automationTriggeredLabel = t("message.automationTriggered");
|
||||
|
||||
@@ -3756,10 +3756,10 @@ function AutomationDetailPanel({
|
||||
: null;
|
||||
const created = job.created_at_ms ? fmtDateTime(job.created_at_ms, locale) : null;
|
||||
const updated = job.updated_at_ms ? fmtDateTime(job.updated_at_ms, locale) : null;
|
||||
const externalTrigger = isExternalTriggerAutomation(job);
|
||||
const localTrigger = isLocalTriggerAutomation(job);
|
||||
const triggerCommand = automationTriggerCommand(job);
|
||||
const message = automationDetailText(job, tx);
|
||||
const messageLabel = externalTrigger
|
||||
const messageLabel = localTrigger
|
||||
? tx("settings.automations.fields.command", "Command")
|
||||
: tx("settings.automations.fields.message", "Message");
|
||||
const schedule = formatAutomationSchedule(job, locale, tx);
|
||||
@@ -3807,7 +3807,7 @@ function AutomationDetailPanel({
|
||||
<div className="text-[11px] font-medium leading-none text-muted-foreground/75">
|
||||
{messageLabel}
|
||||
</div>
|
||||
{externalTrigger && triggerCommand ? (
|
||||
{localTrigger && triggerCommand ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -3833,7 +3833,7 @@ function AutomationDetailPanel({
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 whitespace-pre-wrap break-words text-[13px] leading-6 text-foreground/85",
|
||||
externalTrigger && "font-mono text-[12.5px]",
|
||||
localTrigger && "font-mono text-[12.5px]",
|
||||
!messageExpanded && messageNeedsExpansion && "line-clamp-6",
|
||||
)}
|
||||
>
|
||||
@@ -3940,8 +3940,8 @@ function AutomationActionGroup({
|
||||
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
||||
const canManage = !job.protected;
|
||||
const hasLinkedChat = Boolean(job.origin);
|
||||
const externalTrigger = isExternalTriggerAutomation(job);
|
||||
const canRun = canManage && hasLinkedChat && job.enabled && !job.state.pending && !externalTrigger;
|
||||
const localTrigger = isLocalTriggerAutomation(job);
|
||||
const canRun = canManage && hasLinkedChat && job.enabled && !job.state.pending && !localTrigger;
|
||||
const toggleAction: AutomationAction = job.enabled ? "disable" : "enable";
|
||||
const canToggle = canManage && (job.enabled || hasLinkedChat);
|
||||
const toggleBusy = actionKey === `${toggleAction}:${job.id}`;
|
||||
@@ -3963,7 +3963,7 @@ function AutomationActionGroup({
|
||||
>
|
||||
<Pencil className="h-4 w-4" aria-hidden />
|
||||
</AppsActionButton>
|
||||
{!externalTrigger ? (
|
||||
{!localTrigger ? (
|
||||
<AppsActionButton
|
||||
ariaLabel={tx("settings.automations.runNow", "Run now")}
|
||||
busy={actionKey === `run:${job.id}`}
|
||||
@@ -4095,7 +4095,7 @@ function AutomationEditDialog({
|
||||
const tx = (key: string, fallback: string, values?: Record<string, unknown>) =>
|
||||
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
||||
const [draft, setDraft] = useState<AutomationEditDraft>(() => automationDraftFromJob(null));
|
||||
const externalTrigger = isExternalTriggerAutomation(job);
|
||||
const localTrigger = isLocalTriggerAutomation(job);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(automationDraftFromJob(job));
|
||||
@@ -4145,7 +4145,7 @@ function AutomationEditDialog({
|
||||
/>
|
||||
</label>
|
||||
|
||||
{!externalTrigger ? (
|
||||
{!localTrigger ? (
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.message", "Message")}
|
||||
@@ -4158,7 +4158,7 @@ function AutomationEditDialog({
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
{!externalTrigger ? (
|
||||
{!localTrigger ? (
|
||||
<div className="space-y-2">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.scheduleType", "Schedule type")}
|
||||
@@ -4176,7 +4176,7 @@ function AutomationEditDialog({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!externalTrigger && draft.scheduleKind === "every" ? (
|
||||
{!localTrigger && draft.scheduleKind === "every" ? (
|
||||
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_10rem]">
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
@@ -4217,7 +4217,7 @@ function AutomationEditDialog({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!externalTrigger && draft.scheduleKind === "cron" ? (
|
||||
{!localTrigger && draft.scheduleKind === "cron" ? (
|
||||
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_12rem]">
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
@@ -4244,7 +4244,7 @@ function AutomationEditDialog({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!externalTrigger && draft.scheduleKind === "at" ? (
|
||||
{!localTrigger && draft.scheduleKind === "at" ? (
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.runAt", "Run at")}
|
||||
@@ -4340,11 +4340,11 @@ function AutomationDeleteDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function isExternalTriggerAutomation(job: SessionAutomationJob | null): boolean {
|
||||
function isLocalTriggerAutomation(job: SessionAutomationJob | null): boolean {
|
||||
if (!job) return false;
|
||||
return job.kind === "external_trigger"
|
||||
|| job.payload.kind === "external_trigger"
|
||||
|| job.schedule.kind === "external";
|
||||
return job.kind === "local_trigger"
|
||||
|| job.payload.kind === "local_trigger"
|
||||
|| job.schedule.kind === "local";
|
||||
}
|
||||
|
||||
function automationTriggerCommand(job: SessionAutomationJob): string {
|
||||
@@ -4355,8 +4355,8 @@ function automationSummary(
|
||||
job: SessionAutomationJob,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
return automationTriggerCommand(job) || tx("settings.automations.externalTrigger", "External trigger");
|
||||
if (isLocalTriggerAutomation(job)) {
|
||||
return automationTriggerCommand(job) || tx("settings.automations.localTrigger", "Local trigger");
|
||||
}
|
||||
return job.payload.message || tx("settings.automations.systemTask", "System-managed automation");
|
||||
}
|
||||
@@ -4379,7 +4379,7 @@ function automationStatusKey(
|
||||
if (job.state.pending) return "running";
|
||||
if (!job.enabled) return "paused";
|
||||
if (job.state.last_status === "error") return "failed";
|
||||
if (isExternalTriggerAutomation(job)) return "active";
|
||||
if (isLocalTriggerAutomation(job)) return "active";
|
||||
if (job.delete_after_run && !job.state.next_run_at_ms && job.state.last_status === "ok") {
|
||||
return "completed";
|
||||
}
|
||||
@@ -4443,7 +4443,7 @@ function automationEditDraftError(
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string | null {
|
||||
if (!draft.name.trim()) return tx("settings.automations.validation.nameRequired", "Name is required.");
|
||||
if (isExternalTriggerAutomation(job)) return null;
|
||||
if (isLocalTriggerAutomation(job)) return null;
|
||||
if (!draft.message.trim()) {
|
||||
return tx("settings.automations.validation.messageRequired", "Message is required.");
|
||||
}
|
||||
@@ -4473,7 +4473,7 @@ function automationUpdatePayloadFromDraft(
|
||||
job: SessionAutomationJob | null,
|
||||
): AutomationUpdatePayload | string {
|
||||
const name = draft.name.trim();
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
if (isLocalTriggerAutomation(job)) {
|
||||
if (!name) return "invalid";
|
||||
return { name };
|
||||
}
|
||||
@@ -4604,7 +4604,7 @@ function automationSearchParts(
|
||||
job.payload.message,
|
||||
job.payload.command,
|
||||
job.trigger?.command,
|
||||
isExternalTriggerAutomation(job) ? "trigger external" : null,
|
||||
isLocalTriggerAutomation(job) ? "trigger local" : null,
|
||||
...scheduleParts,
|
||||
automationStatusKey(job),
|
||||
...originParts,
|
||||
@@ -4794,8 +4794,8 @@ function formatAutomationSchedule(
|
||||
})
|
||||
: tx("settings.automations.schedule.cron", "Cron {{expr}}", { expr: job.schedule.expr });
|
||||
}
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
return tx("settings.automations.schedule.external", "External trigger");
|
||||
if (isLocalTriggerAutomation(job)) {
|
||||
return tx("settings.automations.schedule.local", "Local trigger");
|
||||
}
|
||||
return tx("settings.automations.schedule.custom", "Custom schedule");
|
||||
}
|
||||
@@ -4851,8 +4851,8 @@ function formatAutomationNext(
|
||||
): string {
|
||||
if (!job.enabled) return tx("settings.automations.next.paused", "Paused");
|
||||
if (job.state.pending) return tx("settings.automations.next.pending", "Running now");
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
return tx("settings.automations.next.external", "Waiting for trigger");
|
||||
if (isLocalTriggerAutomation(job)) {
|
||||
return tx("settings.automations.next.local", "Waiting for trigger");
|
||||
}
|
||||
if (!job.state.next_run_at_ms) return tx("settings.automations.next.none", "No next run");
|
||||
return relativeTime(job.state.next_run_at_ms);
|
||||
|
||||
@@ -152,6 +152,9 @@ function AutomationRow({ job, now }: { job: SessionAutomationJob; now: number })
|
||||
|
||||
function formatSchedule(job: SessionAutomationJob, t: TFunction) {
|
||||
const locale = currentLocale();
|
||||
if (isLocalTriggerAutomation(job)) {
|
||||
return t("thread.sessionInfo.schedule.local");
|
||||
}
|
||||
if (job.schedule.kind === "at" && job.schedule.at_ms) {
|
||||
return t("thread.sessionInfo.schedule.at", { time: fmtDateTime(job.schedule.at_ms, locale) });
|
||||
}
|
||||
@@ -179,6 +182,9 @@ function formatNextRun(job: SessionAutomationJob, t: TFunction, now: number) {
|
||||
if (job.state.pending) {
|
||||
return { label: t("thread.sessionInfo.next.pending"), title: "" };
|
||||
}
|
||||
if (isLocalTriggerAutomation(job)) {
|
||||
return { label: t("thread.sessionInfo.next.local"), title: "" };
|
||||
}
|
||||
const next = job.state.next_run_at_ms;
|
||||
if (!next) {
|
||||
return { label: t("thread.sessionInfo.next.none"), title: "" };
|
||||
@@ -189,6 +195,12 @@ function formatNextRun(job: SessionAutomationJob, t: TFunction, now: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function isLocalTriggerAutomation(job: SessionAutomationJob): boolean {
|
||||
return job.kind === "local_trigger"
|
||||
|| job.payload.kind === "local_trigger"
|
||||
|| job.schedule.kind === "local";
|
||||
}
|
||||
|
||||
function relativeTimeFrom(value: number, now: number, locale: string): string {
|
||||
let delta = (value - now) / 1000;
|
||||
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "Create one from where it should run so nanobot keeps the right context.",
|
||||
"oneShot": "One-time",
|
||||
"systemTask": "System-managed automation",
|
||||
"localTrigger": "Local trigger",
|
||||
"labels": {
|
||||
"schedule": "Schedule",
|
||||
"next": "Next",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "Weekdays at {{time}}",
|
||||
"hourlyAt": "Hourly at :{{minute}}",
|
||||
"hourlyWindow": "Hourly {{start}}-{{end}} at :{{minute}}",
|
||||
"local": "Local trigger",
|
||||
"custom": "Custom schedule"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Paused",
|
||||
"pending": "Running now",
|
||||
"local": "Waiting for trigger",
|
||||
"none": "No next run"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "Every {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Local trigger",
|
||||
"unknown": "Custom schedule"
|
||||
},
|
||||
"next": {
|
||||
"label": "Next: {{time}}",
|
||||
"disabled": "Paused",
|
||||
"local": "Waiting for trigger",
|
||||
"none": "No next run"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "Every {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Local trigger",
|
||||
"unknown": "Custom schedule"
|
||||
},
|
||||
"next": {
|
||||
"label": "{{time}}",
|
||||
"pending": "Runs shortly",
|
||||
"disabled": "Paused",
|
||||
"local": "Waiting for trigger",
|
||||
"none": "No next run"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "Créala desde donde debe ejecutarse para que nanobot conserve el contexto correcto.",
|
||||
"oneShot": "Una vez",
|
||||
"systemTask": "Automatización administrada por el sistema",
|
||||
"localTrigger": "Activador local",
|
||||
"labels": {
|
||||
"schedule": "Programación",
|
||||
"next": "Siguiente",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "Días laborables a las {{time}}",
|
||||
"hourlyAt": "Cada hora en :{{minute}}",
|
||||
"hourlyWindow": "Cada hora {{start}}-{{end}} en :{{minute}}",
|
||||
"local": "Activador local",
|
||||
"custom": "Programación personalizada"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Pausada",
|
||||
"pending": "Ejecutándose ahora",
|
||||
"local": "Esperando activador",
|
||||
"none": "Sin próxima ejecución"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "Cada {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Activador local",
|
||||
"unknown": "Programación personalizada"
|
||||
},
|
||||
"next": {
|
||||
"label": "Siguiente: {{time}}",
|
||||
"disabled": "Pausada",
|
||||
"local": "Esperando activador",
|
||||
"none": "Sin próxima ejecución"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "Cada {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Activador local",
|
||||
"unknown": "Programación personalizada"
|
||||
},
|
||||
"next": {
|
||||
"label": "Siguiente {{time}}",
|
||||
"pending": "Se ejecutará pronto",
|
||||
"disabled": "En pausa",
|
||||
"local": "Esperando activador",
|
||||
"none": "Sin próxima ejecución"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "Créez-la depuis son point d'exécution pour que nanobot conserve le bon contexte.",
|
||||
"oneShot": "Ponctuelle",
|
||||
"systemTask": "Automatisation gérée par le système",
|
||||
"localTrigger": "Déclencheur local",
|
||||
"labels": {
|
||||
"schedule": "Planning",
|
||||
"next": "Prochaine",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "Jours ouvrés à {{time}}",
|
||||
"hourlyAt": "Toutes les heures à :{{minute}}",
|
||||
"hourlyWindow": "Toutes les heures {{start}}-{{end}} à :{{minute}}",
|
||||
"local": "Déclencheur local",
|
||||
"custom": "Planning personnalisé"
|
||||
},
|
||||
"next": {
|
||||
"paused": "En pause",
|
||||
"pending": "En cours d’exécution",
|
||||
"local": "En attente du déclencheur",
|
||||
"none": "Aucune prochaine exécution"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "Tous les {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Déclencheur local",
|
||||
"unknown": "Planification personnalisée"
|
||||
},
|
||||
"next": {
|
||||
"label": "Prochaine exécution : {{time}}",
|
||||
"disabled": "En pause",
|
||||
"local": "En attente du déclencheur",
|
||||
"none": "Aucune prochaine exécution"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "Toutes les {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Déclencheur local",
|
||||
"unknown": "Planification personnalisée"
|
||||
},
|
||||
"next": {
|
||||
"label": "Prochaine {{time}}",
|
||||
"pending": "Exécution imminente",
|
||||
"disabled": "En pause",
|
||||
"local": "En attente du déclencheur",
|
||||
"none": "Aucune prochaine exécution"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "Buat dari tempat tugas ini berjalan agar nanobot menyimpan konteks yang tepat.",
|
||||
"oneShot": "Satu kali",
|
||||
"systemTask": "Automasi yang dikelola sistem",
|
||||
"localTrigger": "Pemicu lokal",
|
||||
"labels": {
|
||||
"schedule": "Jadwal",
|
||||
"next": "Berikutnya",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "Hari kerja pukul {{time}}",
|
||||
"hourlyAt": "Setiap jam pada :{{minute}}",
|
||||
"hourlyWindow": "Setiap jam {{start}}-{{end}} pada :{{minute}}",
|
||||
"local": "Pemicu lokal",
|
||||
"custom": "Jadwal khusus"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Dijeda",
|
||||
"pending": "Sedang berjalan",
|
||||
"local": "Menunggu pemicu",
|
||||
"none": "Tidak ada jadwal berikutnya"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "Setiap {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Pemicu lokal",
|
||||
"unknown": "Jadwal khusus"
|
||||
},
|
||||
"next": {
|
||||
"label": "Berikutnya: {{time}}",
|
||||
"disabled": "Dijeda",
|
||||
"local": "Menunggu pemicu",
|
||||
"none": "Tidak ada jadwal berikutnya"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "Setiap {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Pemicu lokal",
|
||||
"unknown": "Jadwal khusus"
|
||||
},
|
||||
"next": {
|
||||
"label": "Berikutnya {{time}}",
|
||||
"pending": "Segera berjalan",
|
||||
"disabled": "Dijeda",
|
||||
"local": "Menunggu pemicu",
|
||||
"none": "Tidak ada jadwal berikutnya"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "実行元から作成すると、nanobot が正しいコンテキストを保持できます。",
|
||||
"oneShot": "一回限り",
|
||||
"systemTask": "システム管理の自動タスク",
|
||||
"localTrigger": "ローカルトリガー",
|
||||
"labels": {
|
||||
"schedule": "スケジュール",
|
||||
"next": "次回",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "平日 {{time}}",
|
||||
"hourlyAt": "毎時 :{{minute}}",
|
||||
"hourlyWindow": "{{start}}-{{end}} の毎時 :{{minute}}",
|
||||
"local": "ローカルトリガー",
|
||||
"custom": "カスタムスケジュール"
|
||||
},
|
||||
"next": {
|
||||
"paused": "一時停止",
|
||||
"pending": "実行中",
|
||||
"local": "トリガー待ち",
|
||||
"none": "次回実行なし"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "{{duration}} ごと",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "ローカルトリガー",
|
||||
"unknown": "カスタムスケジュール"
|
||||
},
|
||||
"next": {
|
||||
"label": "次回: {{time}}",
|
||||
"disabled": "一時停止中",
|
||||
"local": "トリガー待ち",
|
||||
"none": "次回実行なし"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "{{duration}}ごと",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "ローカルトリガー",
|
||||
"unknown": "カスタムスケジュール"
|
||||
},
|
||||
"next": {
|
||||
"label": "次回 {{time}}",
|
||||
"pending": "まもなく実行",
|
||||
"disabled": "一時停止",
|
||||
"local": "トリガー待ち",
|
||||
"none": "次回実行なし"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "실행될 위치에서 만들면 nanobot이 올바른 컨텍스트를 유지합니다.",
|
||||
"oneShot": "일회성",
|
||||
"systemTask": "시스템 관리 자동화",
|
||||
"localTrigger": "로컬 트리거",
|
||||
"labels": {
|
||||
"schedule": "일정",
|
||||
"next": "다음",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "평일 {{time}}",
|
||||
"hourlyAt": "매시간 :{{minute}}",
|
||||
"hourlyWindow": "{{start}}-{{end}} 사이 매시간 :{{minute}}",
|
||||
"local": "로컬 트리거",
|
||||
"custom": "사용자 지정 일정"
|
||||
},
|
||||
"next": {
|
||||
"paused": "일시 중지",
|
||||
"pending": "실행 중",
|
||||
"local": "트리거 대기 중",
|
||||
"none": "다음 실행 없음"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "{{duration}}마다",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "로컬 트리거",
|
||||
"unknown": "사용자 지정 일정"
|
||||
},
|
||||
"next": {
|
||||
"label": "다음: {{time}}",
|
||||
"disabled": "일시 중지됨",
|
||||
"local": "트리거 대기 중",
|
||||
"none": "다음 실행 없음"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "{{duration}}마다",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "로컬 트리거",
|
||||
"unknown": "사용자 지정 일정"
|
||||
},
|
||||
"next": {
|
||||
"label": "다음 {{time}}",
|
||||
"pending": "곧 실행됨",
|
||||
"disabled": "일시 중지됨",
|
||||
"local": "트리거 대기 중",
|
||||
"none": "다음 실행 없음"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "Tạo từ nơi tác vụ sẽ chạy để nanobot giữ đúng ngữ cảnh.",
|
||||
"oneShot": "Một lần",
|
||||
"systemTask": "Tự động hóa do hệ thống quản lý",
|
||||
"localTrigger": "Trình kích hoạt cục bộ",
|
||||
"labels": {
|
||||
"schedule": "Lịch",
|
||||
"next": "Tiếp theo",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "Ngày làm việc lúc {{time}}",
|
||||
"hourlyAt": "Mỗi giờ tại :{{minute}}",
|
||||
"hourlyWindow": "Mỗi giờ {{start}}-{{end}} tại :{{minute}}",
|
||||
"local": "Trình kích hoạt cục bộ",
|
||||
"custom": "Lịch tùy chỉnh"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Đã tạm dừng",
|
||||
"pending": "Đang chạy",
|
||||
"local": "Đang chờ kích hoạt",
|
||||
"none": "Không có lần chạy tiếp theo"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "Mỗi {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Trình kích hoạt cục bộ",
|
||||
"unknown": "Lịch tùy chỉnh"
|
||||
},
|
||||
"next": {
|
||||
"label": "Tiếp theo: {{time}}",
|
||||
"disabled": "Đã tạm dừng",
|
||||
"local": "Đang chờ kích hoạt",
|
||||
"none": "Không có lần chạy tiếp theo"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "Mỗi {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "Trình kích hoạt cục bộ",
|
||||
"unknown": "Lịch tùy chỉnh"
|
||||
},
|
||||
"next": {
|
||||
"label": "Tiếp theo {{time}}",
|
||||
"pending": "Sắp chạy",
|
||||
"disabled": "Đã tạm dừng",
|
||||
"local": "Đang chờ kích hoạt",
|
||||
"none": "Không có lần chạy tiếp theo"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "请从它应该运行的来源处创建,这样 nanobot 才能保留正确上下文。",
|
||||
"oneShot": "一次性",
|
||||
"systemTask": "系统管理的自动任务",
|
||||
"localTrigger": "本地触发器",
|
||||
"labels": {
|
||||
"schedule": "计划",
|
||||
"next": "下次",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "工作日 {{time}}",
|
||||
"hourlyAt": "每小时第 {{minute}} 分钟",
|
||||
"hourlyWindow": "{{start}}-{{end}} 点每小时第 {{minute}} 分钟",
|
||||
"local": "本地触发器",
|
||||
"custom": "自定义计划"
|
||||
},
|
||||
"next": {
|
||||
"paused": "已暂停",
|
||||
"pending": "正在运行",
|
||||
"local": "等待触发",
|
||||
"none": "没有下次运行"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "每 {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "本地触发器",
|
||||
"unknown": "自定义计划"
|
||||
},
|
||||
"next": {
|
||||
"label": "下次:{{time}}",
|
||||
"disabled": "已暂停",
|
||||
"local": "等待触发",
|
||||
"none": "没有下次运行"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "每 {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "本地触发器",
|
||||
"unknown": "自定义计划"
|
||||
},
|
||||
"next": {
|
||||
"label": "下次 {{time}}",
|
||||
"pending": "即将执行",
|
||||
"disabled": "已暂停",
|
||||
"local": "等待触发",
|
||||
"none": "没有下次运行"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
"emptyHint": "請從它應該執行的來源處建立,這樣 nanobot 才能保留正確上下文。",
|
||||
"oneShot": "一次性",
|
||||
"systemTask": "系統管理的自動任務",
|
||||
"localTrigger": "本機觸發器",
|
||||
"labels": {
|
||||
"schedule": "排程",
|
||||
"next": "下次",
|
||||
@@ -551,11 +552,13 @@
|
||||
"weekdaysAt": "工作日 {{time}}",
|
||||
"hourlyAt": "每小時第 {{minute}} 分鐘",
|
||||
"hourlyWindow": "{{start}}-{{end}} 點每小時第 {{minute}} 分鐘",
|
||||
"local": "本機觸發器",
|
||||
"custom": "自訂排程"
|
||||
},
|
||||
"next": {
|
||||
"paused": "已暫停",
|
||||
"pending": "正在執行",
|
||||
"local": "等待觸發",
|
||||
"none": "沒有下次執行"
|
||||
},
|
||||
"message": {
|
||||
@@ -692,11 +695,13 @@
|
||||
"every": "每 {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "本機觸發器",
|
||||
"unknown": "自訂計畫"
|
||||
},
|
||||
"next": {
|
||||
"label": "下次:{{time}}",
|
||||
"disabled": "已暫停",
|
||||
"local": "等待觸發",
|
||||
"none": "沒有下次執行"
|
||||
}
|
||||
},
|
||||
@@ -791,12 +796,14 @@
|
||||
"every": "每 {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"local": "本機觸發器",
|
||||
"unknown": "自訂計畫"
|
||||
},
|
||||
"next": {
|
||||
"label": "下次 {{time}}",
|
||||
"pending": "即將執行",
|
||||
"disabled": "已暫停",
|
||||
"local": "等待觸發",
|
||||
"none": "沒有下次執行"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface UIMediaAttachment {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface UIMessageSource { kind: "cron" | "trigger" | string; label?: string; }
|
||||
export interface UIMessageSource { kind: "cron" | "local_trigger" | "trigger" | string; label?: string; }
|
||||
|
||||
export interface UIMessage {
|
||||
id: string;
|
||||
@@ -104,9 +104,9 @@ export interface SessionAutomationJob {
|
||||
delete_after_run?: boolean;
|
||||
created_at_ms?: number | null;
|
||||
updated_at_ms?: number | null;
|
||||
kind?: "external_trigger" | "cron" | string;
|
||||
kind?: "local_trigger" | "cron" | string;
|
||||
schedule: {
|
||||
kind: "at" | "every" | "cron" | "external" | string;
|
||||
kind: "at" | "every" | "cron" | "local" | string;
|
||||
at_ms?: number | null;
|
||||
every_ms?: number | null;
|
||||
expr?: string | null;
|
||||
@@ -114,7 +114,7 @@ export interface SessionAutomationJob {
|
||||
};
|
||||
payload: {
|
||||
message: string;
|
||||
kind?: "agent_turn" | "system_event" | "external_trigger" | string;
|
||||
kind?: "agent_turn" | "system_event" | "local_trigger" | string;
|
||||
command?: string;
|
||||
};
|
||||
state: {
|
||||
|
||||
Reference in New Issue
Block a user