Files
chatgpt-on-wechat/channel/feishu/feishu_scheduler_card.py
2026-07-17 12:54:26 +08:00

186 lines
6.3 KiB
Python

"""Feishu scheduler card rendering and callback handling."""
from __future__ import annotations
from typing import Any, Dict, Iterable, List, Set
_MAX_TASKS = 20
def tasks_for_receivers(tasks: Iterable[dict], receivers: Set[str]) -> List[dict]:
"""Return Feishu tasks owned by one of the callback's trusted receivers."""
visible = []
for task in tasks:
action = task.get("action") or {}
if action.get("channel_type") != "feishu":
continue
if action.get("receiver") in receivers:
visible.append(task)
return visible
def build_scheduler_card(tasks: Iterable[dict]) -> Dict[str, Any]:
"""Build a Card 2.0 task list with explicit, idempotent actions."""
task_list = list(tasks)
elements: List[Dict[str, Any]] = []
if not task_list:
elements.append({"tag": "markdown", "content": "No scheduled tasks in this chat."})
else:
for index, task in enumerate(task_list[:_MAX_TASKS]):
if index:
elements.append({"tag": "hr"})
task_id = str(task.get("id") or "")
receiver = str((task.get("action") or {}).get("receiver") or "")
enabled = task.get("enabled", True)
status = "Enabled" if enabled else "Disabled"
next_run = str(task.get("next_run_at") or "Unknown").replace("T", " ")
elements.append(
{
"tag": "markdown",
"content": "**{}** · {}\n`{}` · {}\nNext: {}".format(
task.get("name") or "Unnamed task",
status,
task_id,
_format_schedule(task.get("schedule") or {}),
next_run,
),
}
)
toggle_action = "disable" if enabled else "enable"
toggle_text = "Disable" if enabled else "Enable"
toggle_type = "default" if enabled else "primary"
elements.append(
{
"tag": "column_set",
"columns": [
{
"tag": "column",
"elements": [
_button(
toggle_text,
toggle_type,
toggle_action,
task_id,
receiver,
)
],
},
{
"tag": "column",
"elements": [_button("Delete", "danger", "delete", task_id, receiver)],
},
],
}
)
hidden = len(task_list) - _MAX_TASKS
if hidden > 0:
elements.extend(
[
{"tag": "hr"},
{
"tag": "markdown",
"content": "{} more tasks are hidden.".format(hidden),
"text_size": "notation",
},
]
)
return {
"schema": "2.0",
"config": {"update_multi": True, "enable_forward_interaction": False},
"header": {
"template": "blue",
"title": {"tag": "plain_text", "content": "Scheduled tasks"},
},
"body": {"elements": elements},
}
def handle_scheduler_action(
value: Dict[str, Any], task_store: Any, allowed_receivers: Set[str]
) -> Dict[str, Any]:
"""Apply an owned scheduler action and return a Feishu callback response."""
if value.get("cowagent") != "scheduler":
return {}
task_id = str(value.get("task_id") or "")
action = value.get("action")
if not task_id or action not in {"enable", "disable", "delete"}:
return _toast("error", "Invalid scheduler action")
task = task_store.get_task(task_id)
if not task:
return _response(
"info",
"Task no longer exists",
build_scheduler_card(tasks_for_receivers(task_store.list_tasks(), allowed_receivers)),
)
task_receiver = (task.get("action") or {}).get("receiver")
task_channel = (task.get("action") or {}).get("channel_type")
value_receiver = value.get("receiver")
if (
task_channel != "feishu"
or task_receiver not in allowed_receivers
or (value_receiver and value_receiver != task_receiver)
):
return _toast("error", "Task is not available in this chat")
try:
if action == "delete":
task_store.delete_task(task_id)
message = "Task deleted"
else:
enabled = action == "enable"
task_store.enable_task(task_id, enabled)
message = "Task enabled" if enabled else "Task disabled"
except (OSError, ValueError) as exc:
return _toast("error", "Task update failed: {}".format(exc))
visible = tasks_for_receivers(task_store.list_tasks(), allowed_receivers)
return _response("success", message, build_scheduler_card(visible))
def _response(toast_type: str, content: str, card: Dict[str, Any]) -> Dict[str, Any]:
response = _toast(toast_type, content)
response["card"] = {"type": "raw", "data": card}
return response
def _toast(toast_type: str, content: str) -> Dict[str, Any]:
return {"toast": {"type": toast_type, "content": content}}
def _button(
text: str,
button_type: str,
action: str,
task_id: str,
receiver: str,
) -> Dict[str, Any]:
return {
"tag": "button",
"text": {"tag": "plain_text", "content": text},
"type": button_type,
"value": {
"cowagent": "scheduler",
"action": action,
"task_id": task_id,
"receiver": receiver,
},
}
def _format_schedule(schedule: Dict[str, Any]) -> str:
schedule_type = schedule.get("type")
if schedule_type == "cron":
return "cron {}".format(schedule.get("expression") or "?")
if schedule_type == "interval":
return "every {}s".format(schedule.get("seconds") or "?")
if schedule_type == "once":
return "once at {}".format(schedule.get("run_at") or "?")
return str(schedule_type or "unknown schedule")