Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-20) — Multi-Channel Webhook Notifications](#whats-new-2026-06-20--multi-channel-webhook-notifications)
- [What's new (2026-06-20) — Outbound CloudEvents Emitter](#whats-new-2026-06-20--outbound-cloudevents-emitter)
- [What's new (2026-06-20) — Environment-Scoped Typed Asset Store](#whats-new-2026-06-20--environment-scoped-typed-asset-store)
- [What's new (2026-06-20) — Task / Process Mining (Automation-Candidate Discovery)](#whats-new-2026-06-20--task--process-mining-automation-candidate-discovery)
Expand Down Expand Up @@ -102,6 +103,12 @@

---

## What's new (2026-06-20) — Multi-Channel Webhook Notifications

Alert Teams/Discord/Slack/webhook. Full reference: [`docs/source/Eng/doc/new_features/v50_features_doc.rst`](docs/source/Eng/doc/new_features/v50_features_doc.rst).

- **`notify_webhook` / `WebhookChannel`** (`AC_notify_webhook`, `ac_notify_webhook`): `notify` was desktop-toast only and ChatOps shipped Slack only — this sends to **Slack / Discord / Microsoft Teams / raw** webhooks, building the transport-shaped payload (Slack & Teams MessageCard use `text`, Discord uses `content`) and POSTing via the egress-guarded HTTP client. The `poster` transport is injectable (or `set_default_poster`), so sending is unit-tested with no network.

## What's new (2026-06-20) — Outbound CloudEvents Emitter

Emit run/automation events as CloudEvents. Full reference: [`docs/source/Eng/doc/new_features/v49_features_doc.rst`](docs/source/Eng/doc/new_features/v49_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-20) — 多通道 Webhook 通知](#本次更新-2026-06-20--多通道-webhook-通知)
- [本次更新 (2026-06-20) — 对外 CloudEvents 发送器](#本次更新-2026-06-20--对外-cloudevents-发送器)
- [本次更新 (2026-06-20) — 环境范围的带类型资产存储](#本次更新-2026-06-20--环境范围的带类型资产存储)
- [本次更新 (2026-06-20) — 任务 / 流程挖掘(自动化候选发现)](#本次更新-2026-06-20--任务--流程挖掘自动化候选发现)
Expand Down Expand Up @@ -101,6 +102,12 @@

---

## 本次更新 (2026-06-20) — 多通道 Webhook 通知

通知 Teams/Discord/Slack/webhook。完整参考:[`docs/source/Zh/doc/new_features/v50_features_doc.rst`](../docs/source/Zh/doc/new_features/v50_features_doc.rst)。

- **`notify_webhook` / `WebhookChannel`**(`AC_notify_webhook`、`ac_notify_webhook`):`notify` 仅限桌面弹窗、ChatOps 只内建 Slack —— 本功能可发送到 **Slack / Discord / Microsoft Teams / raw** webhook,组出对应传输的载荷(Slack 与 Teams MessageCard 用 `text`,Discord 用 `content`)并通过受出口守卫保护的 HTTP 客户端 POST。`poster` 传输可注入(或 `set_default_poster`),因此发送在无网络下即可单元测试。

## 本次更新 (2026-06-20) — 对外 CloudEvents 发送器

将运行/自动化事件以 CloudEvents 发送。完整参考:[`docs/source/Zh/doc/new_features/v49_features_doc.rst`](../docs/source/Zh/doc/new_features/v49_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-20) — 多通道 Webhook 通知](#本次更新-2026-06-20--多通道-webhook-通知)
- [本次更新 (2026-06-20) — 對外 CloudEvents 發送器](#本次更新-2026-06-20--對外-cloudevents-發送器)
- [本次更新 (2026-06-20) — 環境範圍的具型別資產儲存](#本次更新-2026-06-20--環境範圍的具型別資產儲存)
- [本次更新 (2026-06-20) — 任務 / 流程探勘(自動化候選發現)](#本次更新-2026-06-20--任務--流程探勘自動化候選發現)
Expand Down Expand Up @@ -101,6 +102,12 @@

---

## 本次更新 (2026-06-20) — 多通道 Webhook 通知

通知 Teams/Discord/Slack/webhook。完整參考:[`docs/source/Zh/doc/new_features/v50_features_doc.rst`](../docs/source/Zh/doc/new_features/v50_features_doc.rst)。

- **`notify_webhook` / `WebhookChannel`**(`AC_notify_webhook`、`ac_notify_webhook`):`notify` 僅限桌面快顯、ChatOps 只內建 Slack —— 本功能可發送到 **Slack / Discord / Microsoft Teams / raw** webhook,組出對應傳輸的酬載(Slack 與 Teams MessageCard 用 `text`,Discord 用 `content`)並透過受出口守衛保護的 HTTP 用戶端 POST。`poster` 傳輸可注入(或 `set_default_poster`),因此發送在無網路下即可單元測試。

## 本次更新 (2026-06-20) — 對外 CloudEvents 發送器

將執行/自動化事件以 CloudEvents 發送。完整參考:[`docs/source/Zh/doc/new_features/v49_features_doc.rst`](../docs/source/Zh/doc/new_features/v49_features_doc.rst)。
Expand Down
42 changes: 42 additions & 0 deletions docs/source/Eng/doc/new_features/v50_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Multi-Channel Webhook Notifications
===================================

The built-in ``notify`` is desktop-toast only, and ChatOps shipped Slack as the
only transport — but unattended runs want to alert Microsoft Teams, Discord, or a
generic incoming webhook too. Each is a simple JSON POST with a transport-shaped
payload (Slack and a Teams MessageCard use ``text``, Discord uses ``content``);
``notify_webhook`` builds the right body and POSTs it through the egress-guarded
HTTP client.

The transport is injectable (a ``poster`` callable or a module-level default),
so sending is unit-testable with no network. Pure standard library; imports no
``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import notify_webhook, WebhookChannel

notify_webhook("https://hooks.slack.com/...", "Run finished", transport="slack")
notify_webhook("https://discord.com/api/webhooks/...", "Build broke",
transport="discord", title="CI")
notify_webhook("https://prod.webhook.office.com/...", "Deploy done",
transport="teams", title="Release")

chan = WebhookChannel("https://hooks.example.com/x", transport="raw")
result = chan.send("hello") # -> WebhookResult(ok, status, transport)

``transport`` is ``slack`` / ``discord`` / ``teams`` / ``raw``; the result's
``ok`` reflects a 2xx status. Pass a ``poster(url, payload) -> status`` to
``WebhookChannel`` / ``notify_webhook`` (or install one with
``set_default_poster``) to route through a custom transport or a test fake.

Executor command
----------------

``AC_notify_webhook`` takes ``url``, ``text`` (+ optional ``transport`` /
``title``) and returns ``{ok, status, transport}``. The same operation is exposed
as the MCP tool ``ac_notify_webhook`` and as a Script Builder command under
**Tools**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v47_features_doc
doc/new_features/v48_features_doc
doc/new_features/v49_features_doc
doc/new_features/v50_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
38 changes: 38 additions & 0 deletions docs/source/Zh/doc/new_features/v50_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
多通道 Webhook 通知
===================

內建的 ``notify`` 僅限桌面快顯,而 ChatOps 只內建 Slack 一種傳輸 —— 但無人值守的執行也
想通知 Microsoft Teams、Discord 或一般的 incoming webhook。每一種都是簡單的 JSON POST,
搭配對應傳輸的酬載結構(Slack 與 Teams MessageCard 用 ``text``,Discord 用
``content``);``notify_webhook`` 會組出正確的本文,並透過受出口守衛保護的 HTTP 用戶端
POST 出去。

傳輸可注入(``poster`` 可呼叫物件或模組層級預設),因此發送在無網路下即可單元測試。純標
準函式庫;不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import notify_webhook, WebhookChannel

notify_webhook("https://hooks.slack.com/...", "Run finished", transport="slack")
notify_webhook("https://discord.com/api/webhooks/...", "Build broke",
transport="discord", title="CI")
notify_webhook("https://prod.webhook.office.com/...", "Deploy done",
transport="teams", title="Release")

chan = WebhookChannel("https://hooks.example.com/x", transport="raw")
result = chan.send("hello") # -> WebhookResult(ok, status, transport)

``transport`` 為 ``slack`` / ``discord`` / ``teams`` / ``raw``;結果的 ``ok`` 反映 2xx 狀
態。可傳入 ``poster(url, payload) -> status`` 給 ``WebhookChannel`` / ``notify_webhook``
(或以 ``set_default_poster`` 安裝一個)以走自訂傳輸或測試假物件。

執行器指令
----------

``AC_notify_webhook`` 接受 ``url``、``text``(以及選用的 ``transport`` / ``title``),回傳
``{ok, status, transport}``。相同操作亦提供為 MCP 工具 ``ac_notify_webhook``,以及 Script
Builder 中 **Tools** 分類下的指令。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v47_features_doc
doc/new_features/v48_features_doc
doc/new_features/v49_features_doc
doc/new_features/v50_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@
from je_auto_control.utils.events import (
EventEmitter, post_cloudevent, to_cloudevent,
)
# Outbound chat/webhook notifications (Slack/Discord/Teams/raw)
from je_auto_control.utils.notify_channels import (
WebhookChannel, WebhookResult, notify_webhook, set_default_poster,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -733,6 +737,7 @@ def start_autocontrol_gui(*args, **kwargs):
"rank_automation_candidates",
"Asset", "AssetStore", "AssetValue", "active_environment",
"EventEmitter", "post_cloudevent", "to_cloudevent",
"WebhookChannel", "WebhookResult", "notify_webhook", "set_default_poster",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
13 changes: 13 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,19 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="Wrap data in a CloudEvents envelope; optionally POST it.",
))
specs.append(CommandSpec(
"AC_notify_webhook", "Tools", "Notify: Webhook/Chat",
fields=(
FieldSpec("url", FieldType.STRING,
placeholder="https://hooks.example.com/..."),
FieldSpec("text", FieldType.STRING),
FieldSpec("transport", FieldType.ENUM, optional=True,
default="raw",
choices=("raw", "slack", "discord", "teams")),
FieldSpec("title", FieldType.STRING, optional=True),
),
description="Send a Slack/Discord/Teams/raw webhook notification.",
))
specs.append(CommandSpec(
"AC_generate_sop", "Report", "Generate SOP Document",
fields=(
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3287,6 +3287,15 @@ def _emit_event(event_type: str, data: Any = None,
return result


def _notify_webhook(url: str, text: str, transport: str = "raw",
title: Optional[str] = None) -> Dict[str, Any]:
"""Adapter: send a chat/webhook notification (slack/discord/teams/raw)."""
from je_auto_control.utils.notify_channels import notify_webhook
outcome = notify_webhook(url, text, transport=transport, title=title)
return {"ok": outcome.ok, "status": outcome.status,
"transport": outcome.transport}


class Executor:
"""
Executor
Expand Down Expand Up @@ -3566,6 +3575,7 @@ def __init__(self):
"AC_get_asset": _get_asset,
"AC_list_assets": _list_assets,
"AC_emit_event": _emit_event,
"AC_notify_webhook": _notify_webhook,
"AC_a11y_record_start": _a11y_record_start,
"AC_a11y_record_stop": _a11y_record_stop,
"AC_a11y_record_events": _a11y_record_events,
Expand Down
22 changes: 21 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3180,6 +3180,26 @@ def events_tools() -> List[MCPTool]:
]


def notify_channel_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_notify_webhook",
description=("Send a chat/webhook notification. 'transport' shapes "
"the payload: slack/discord/teams/raw (Slack & Teams "
"MessageCard use text, Discord uses content). POSTs via "
"the egress-guarded HTTP client. Returns {ok, status, "
"transport}."),
input_schema=schema(
{"url": {"type": "string"}, "text": {"type": "string"},
"transport": {"type": "string",
"enum": ["raw", "slack", "discord", "teams"]},
"title": {"type": "string"}}, ["url", "text"]),
handler=h.notify_webhook,
annotations=SIDE_EFFECT_ONLY,
),
]


def unattended_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -4242,7 +4262,7 @@ def media_assert_tools() -> List[MCPTool]:
trajectory_eval_tools, compliance_tools, agent_trace_tools,
video_report_tools, fuzzy_tools, artifact_store_tools, image_dedup_tools,
locale_tools, voice_tools, coordinate_space_tools, loop_guard_tools,
process_mining_tools, asset_tools, events_tools,
process_mining_tools, asset_tools, events_tools, notify_channel_tools,
screen_record_tools,
process_and_shell_tools, remote_desktop_tools, gamepad_tools,
usb_passthrough_tools, assertion_tools, data_source_tools,
Expand Down
8 changes: 8 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,14 @@ def emit_event(event_type, data=None, source="je_auto_control",
return result


def notify_webhook(url, text, transport="raw", title=None):
from je_auto_control.utils.notify_channels import (
notify_webhook as _notify)
outcome = _notify(url, text, transport=transport, title=title)
return {"ok": outcome.ok, "status": outcome.status,
"transport": outcome.transport}


def vlm_locate(description: str,
screen_region: Optional[List[int]] = None,
model: Optional[str] = None) -> Optional[List[int]]:
Expand Down
8 changes: 8 additions & 0 deletions je_auto_control/utils/notify_channels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Outbound chat/webhook notifications (Slack/Discord/Teams/raw)."""
from je_auto_control.utils.notify_channels.notify_channels import (
WebhookChannel, WebhookResult, notify_webhook, set_default_poster,
)

__all__ = [
"WebhookChannel", "WebhookResult", "notify_webhook", "set_default_poster",
]
92 changes: 92 additions & 0 deletions je_auto_control/utils/notify_channels/notify_channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Send notifications to chat/webhook endpoints (Slack/Discord/Teams/raw).

The built-in ``notify`` is desktop-toast only and ChatOps shipped Slack as the
only transport; unattended runs want to alert Teams/Discord/generic webhooks
too. Each is a simple JSON POST with a transport-specific payload shape — Slack
and a Teams MessageCard use ``text``, Discord uses ``content`` — so this builds
the right body and POSTs it through the egress-guarded HTTP client.

The transport is injectable (a ``poster`` callable or a module-level default),
so sending is unit-testable with no network. Pure standard library; imports no
``PySide6``.
"""
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional

TRANSPORT_RAW = "raw"
TRANSPORT_SLACK = "slack"
TRANSPORT_DISCORD = "discord"
TRANSPORT_TEAMS = "teams"

_STATE: Dict[str, Any] = {"poster": None}


@dataclass(frozen=True)
class WebhookResult:
"""The outcome of a webhook notification."""

ok: bool
status: int
transport: str


def set_default_poster(poster: Optional[Callable[[str, Dict[str, Any]], int]]
) -> None:
"""Install a module-level transport ``poster(url, payload) -> status``."""
_STATE["poster"] = poster


def _payload(transport: str, text: str, title: Optional[str]) -> Dict[str, Any]:
if transport == TRANSPORT_DISCORD:
body = f"**{title}**\n{text}" if title else text
return {"content": body}
if transport == TRANSPORT_TEAMS:
# The @context below is Microsoft Teams' fixed MessageCard schema
# identifier (required verbatim), not a URL that is ever fetched.
card: Dict[str, Any] = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions", # NOSONAR python:S5332
"text": text,
}
if title:
card["title"] = title
return card
if transport == TRANSPORT_SLACK:
return {"text": f"*{title}*\n{text}" if title else text}
payload: Dict[str, Any] = {"text": text}
if title:
payload["title"] = title
return payload


def _default_poster(url: str, payload: Dict[str, Any]) -> int:
from je_auto_control.utils.http_client.http_client import http_request
response = http_request(url, method="POST", json_body=payload, timeout=10.0)
return int(response.get("status", 0))


class WebhookChannel:
"""A notification channel for one webhook URL and transport."""

def __init__(self, url: str, *, transport: str = TRANSPORT_RAW,
poster: Optional[Callable[[str, Dict[str, Any]], int]] = None
) -> None:
"""``transport`` shapes the payload; ``poster`` overrides the sender."""
self._url = url
self._transport = transport
self._poster = poster

def send(self, text: str, *, title: Optional[str] = None) -> WebhookResult:
"""Post ``text`` (and optional ``title``) to the channel."""
poster = self._poster or _STATE["poster"] or _default_poster
status = int(poster(self._url, _payload(self._transport, text, title)))
return WebhookResult(200 <= status < 300, status, self._transport)


def notify_webhook(url: str, text: str, *, transport: str = TRANSPORT_RAW,
title: Optional[str] = None,
poster: Optional[Callable[[str, Dict[str, Any]], int]] = None
) -> WebhookResult:
"""Send a one-off webhook notification; return a :class:`WebhookResult`."""
return WebhookChannel(url, transport=transport, poster=poster).send(
text, title=title)
Loading
Loading