Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

260 lines
8.7 KiB
Python

"""Task execution with claim-based dedup and per-provider delivery."""
from __future__ import annotations
import logging
import re
from platform.scheduler.claim_store import complete_run, try_claim
from platform.scheduler.credentials import (
resolve_discord_credentials,
resolve_slack_credentials,
resolve_telegram_credentials,
)
from platform.scheduler.tasks import build_message
from platform.scheduler.types import Provider, ScheduledTask, TaskStatus
logger = logging.getLogger(__name__)
# Strip HTML tags for providers that don't support them
_HTML_TAG_RE = re.compile(r"<[^>]+>")
def execute_task(
task: ScheduledTask,
fire_time: str,
) -> bool:
"""Execute a scheduled task with claim-based dedup.
Args:
task: The scheduled task definition.
fire_time: The canonical fire time string (UTC, minute-precision) from the
scheduler trigger, used as the dedup key.
Returns:
True if the task was executed and delivered successfully.
False if the claim was lost (another instance handled it) or delivery failed.
"""
# Attempt to claim this execution slot
if not try_claim(task.id, fire_time):
logger.info(
"Task %s fire_time=%s already claimed by another instance",
task.id,
fire_time,
)
return False
logger.info("Executing task %s (kind=%s, fire_time=%s)", task.id, task.kind, fire_time)
_emit_analytics_started(task)
# Build the message
try:
message = build_message(task)
except RuntimeError as exc:
# Pipeline failures — record without leaking details to chat
_record_failure(task, fire_time, str(exc))
return False
except Exception as exc:
_record_failure(task, fire_time, f"Message build error: {type(exc).__name__}")
return False
# Deliver to the configured provider
ok, error, message_id = _deliver(task, message)
if ok:
complete_run(
task.id,
fire_time,
status=TaskStatus.SUCCESS,
posted_message_id=message_id,
provider=task.provider.value,
)
_emit_analytics(task, TaskStatus.SUCCESS)
logger.info("Task %s delivered successfully (message_id=%s)", task.id, message_id)
return True
else:
_record_failure(task, fire_time, error)
return False
def _deliver(
task: ScheduledTask,
message: str,
) -> tuple[bool, str, str]:
"""Route delivery to the appropriate provider.
Returns (success, error, message_id).
"""
if task.provider == Provider.TELEGRAM:
return _deliver_telegram(task, message)
elif task.provider == Provider.SLACK:
return _deliver_slack(task, message)
elif task.provider == Provider.DISCORD:
return _deliver_discord(task, message)
else:
return False, f"Unsupported provider: {task.provider}", ""
def _strip_html(text: str) -> str:
"""Strip HTML tags for providers that use plain text or Markdown."""
return _HTML_TAG_RE.sub("", text)
def _deliver_telegram(task: ScheduledTask, message: str) -> tuple[bool, str, str]:
"""Deliver via Telegram using the truncation helper then posting directly.
Uses truncate_for_telegram_html to respect the 4096-char limit, then
posts via post_telegram_message (no reply_to — new top-level message).
"""
creds = resolve_telegram_credentials(task.params)
bot_token = creds.get("bot_token", "")
if not bot_token or not task.chat_id:
return False, "Missing bot_token or chat_id for Telegram", ""
from integrations.telegram.delivery import (
post_telegram_message,
truncate_for_telegram_html,
)
from integrations.telegram.formatting import markdown_to_telegram_html
html_message = markdown_to_telegram_html(message)
truncated = truncate_for_telegram_html(html_message, 4096, suffix="…")
ok, error, msg_id = post_telegram_message(task.chat_id, truncated, bot_token, parse_mode="HTML")
if ok:
return True, "", msg_id
return False, error, ""
def _deliver_slack(task: ScheduledTask, message: str) -> tuple[bool, str, str]:
"""Deliver via Slack using the shared Slack delivery helper when possible."""
creds = resolve_slack_credentials(task.params)
access_token = creds.get("access_token", "")
webhook_url = creds.get("webhook_url", "")
# Strip HTML tags — Slack uses mrkdwn, not HTML
plain_message = _strip_html(message)
if access_token and task.chat_id:
# Direct API post as a new top-level message
from platform.notifications.delivery_transport import post_json
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
payload = {
"channel": task.chat_id,
"text": plain_message,
}
response = post_json(
url="https://slack.com/api/chat.postMessage",
payload=payload,
headers=headers,
)
if not response.ok:
return False, f"Slack API error: {response.error}", ""
if not 200 <= response.status_code < 300:
error_text = response.text[:200] if response.text else f"HTTP {response.status_code}"
return False, f"Slack HTTP error: {error_text}", ""
if response.data.get("ok") is not True:
error = response.data.get("error", "unknown")
return False, f"Slack error: {error}", ""
msg_ts = str(response.data.get("ts", ""))
return True, "", msg_ts
if webhook_url:
from integrations.slack.delivery import send_slack_webhook_message
ok, error = send_slack_webhook_message(
plain_message,
webhook_url=webhook_url,
)
return ok, error, ""
if not task.chat_id:
return False, "Missing chat_id or webhook_url for Slack delivery", ""
return (
False,
"Scheduled tasks require Slack webhook_url or bot access_token",
"",
)
def _deliver_discord(task: ScheduledTask, message: str) -> tuple[bool, str, str]:
"""Deliver via Discord using the existing report helper (handles embed truncation)."""
creds = resolve_discord_credentials(task.params)
bot_token = creds.get("bot_token", "")
if not bot_token or not task.chat_id:
return False, "Missing bot_token or channel_id for Discord", ""
from integrations.discord.delivery import send_discord_report
# Strip HTML tags — Discord uses embeds, not HTML
plain_message = _strip_html(message)
discord_ctx = {
"channel_id": task.chat_id,
"bot_token": bot_token,
# No thread_id — scheduled deliveries post to the channel directly
}
ok, error = send_discord_report(plain_message, discord_ctx)
return ok, error, ""
def _record_failure(task: ScheduledTask, fire_time: str, error: str) -> None:
"""Record a failed execution in the claim store and emit analytics."""
complete_run(
task.id,
fire_time,
status=TaskStatus.FAILED,
error=error,
provider=task.provider.value,
)
_emit_analytics(task, TaskStatus.FAILED, error=error)
logger.warning("Task %s failed: %s", task.id, error)
def _emit_analytics_started(task: ScheduledTask) -> None:
"""Emit SCHEDULED_TASK_STARTED event after a claim is won."""
try:
from platform.analytics.events import Event
from platform.analytics.provider import Properties, get_analytics
properties: Properties = {
"task_id": task.id,
"task_kind": task.kind.value,
"provider": task.provider.value,
}
get_analytics().capture(Event.SCHEDULED_TASK_STARTED, properties)
except Exception:
logger.debug("Failed to emit analytics for task %s", task.id, exc_info=True)
def _emit_analytics(task: ScheduledTask, status: TaskStatus, error: str = "") -> None:
"""Emit analytics event for task execution completion."""
try:
from platform.analytics.events import Event
from platform.analytics.provider import Properties, get_analytics
event_name = (
Event.SCHEDULED_TASK_COMPLETED
if status == TaskStatus.SUCCESS
else Event.SCHEDULED_TASK_FAILED
)
properties: Properties = {
"task_id": task.id,
"task_kind": task.kind.value,
"provider": task.provider.value,
"status": status.value,
}
if error:
properties["error"] = error[:200]
get_analytics().capture(event_name, properties)
except Exception:
# Analytics must never crash the scheduler
logger.debug("Failed to emit analytics for task %s", task.id, exc_info=True)
__all__ = ["execute_task"]