mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 21:07:28 +08:00
186 lines
6.3 KiB
Python
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")
|