Skip to content

Commit 24f568b

Browse files
authored
feat: future task UI (#8559)
* feat: future task UI * fix: update filter label for UMO in English and Chinese locales * feat: enhance cron job management with delivery target handling and UI improvements * fix: update session label to indicate optional delivery target * feat: add tooltip for last run time and error in cron job display
1 parent e5d7b43 commit 24f568b

10 files changed

Lines changed: 1575 additions & 624 deletions

File tree

astrbot/core/cron/manager.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from astrbot.core.db import BaseDatabase
1616
from astrbot.core.db.po import CronJob
1717
from astrbot.core.platform.message_session import MessageSession
18+
from astrbot.core.platform.message_type import MessageType
1819
from astrbot.core.provider.entites import ProviderRequest
1920
from astrbot.core.utils.history_saver import persist_agent_history
2021

@@ -200,9 +201,18 @@ def _get_next_run_time(self, job_id: str):
200201
return None
201202
return aps_job.next_run_time.astimezone(timezone.utc)
202203

203-
async def _run_job(self, job_id: str) -> None:
204+
async def run_job_now(self, job_id: str) -> None:
205+
await self._run_job(job_id, ignore_enabled=True, delete_run_once=False)
206+
207+
async def _run_job(
208+
self,
209+
job_id: str,
210+
*,
211+
ignore_enabled: bool = False,
212+
delete_run_once: bool = True,
213+
) -> None:
204214
job = await self.db.get_cron_job(job_id)
205-
if not job or not job.enabled:
215+
if not job or (not job.enabled and not ignore_enabled):
206216
return
207217
start_time = datetime.now(timezone.utc)
208218
await self.db.update_cron_job(
@@ -230,7 +240,7 @@ async def _run_job(self, job_id: str) -> None:
230240
last_error=last_error,
231241
next_run_time=next_run,
232242
)
233-
if job.run_once:
243+
if job.run_once and delete_run_once:
234244
# one-shot: remove after execution regardless of success
235245
await self.delete_job(job_id)
236246

@@ -245,9 +255,14 @@ async def _run_basic_job(self, job: CronJob) -> None:
245255

246256
async def _run_active_agent_job(self, job: CronJob, start_time: datetime) -> None:
247257
payload = job.payload or {}
248-
session_str = payload.get("session")
249-
if not session_str:
250-
raise ValueError("ActiveAgentCronJob missing session.")
258+
delivery_session_str = str(payload.get("session") or "").strip()
259+
session_str = delivery_session_str or str(
260+
MessageSession(
261+
platform_name="cron",
262+
message_type=MessageType.OTHER_MESSAGE,
263+
session_id=job.job_id,
264+
)
265+
)
251266
note = payload.get("note") or job.description or job.name
252267

253268
extras = {
@@ -262,7 +277,7 @@ async def _run_active_agent_job(self, job: CronJob, start_time: datetime) -> Non
262277
"run_at": (
263278
job.payload.get("run_at") if isinstance(job.payload, dict) else None
264279
),
265-
"session": session_str,
280+
"session": delivery_session_str,
266281
},
267282
"cron_payload": payload,
268283
}
@@ -271,6 +286,7 @@ async def _run_active_agent_job(self, job: CronJob, start_time: datetime) -> Non
271286
message=note,
272287
session_str=session_str,
273288
extras=extras,
289+
delivery_session_str=delivery_session_str,
274290
)
275291

276292
async def _woke_main_agent(
@@ -279,6 +295,7 @@ async def _woke_main_agent(
279295
message: str,
280296
session_str: str,
281297
extras: dict,
298+
delivery_session_str: str = "",
282299
) -> None:
283300
"""Woke the main agent to handle the cron job message."""
284301
from astrbot.core.astr_main_agent import (
@@ -353,11 +370,12 @@ async def _woke_main_agent(
353370
"Output using same language as previous conversation. "
354371
"After completing your task, summarize and output your actions and results."
355372
)
356-
if not req.func_tool:
357-
req.func_tool = ToolSet()
358-
req.func_tool.add_tool(
359-
self.ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool)
360-
)
373+
if delivery_session_str:
374+
if not req.func_tool:
375+
req.func_tool = ToolSet()
376+
req.func_tool.add_tool(
377+
self.ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool)
378+
)
361379

362380
result = await build_main_agent(
363381
event=cron_event, plugin_context=self.ctx, config=config, req=req

astrbot/dashboard/routes/cron.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import traceback
23
from datetime import datetime, timezone
34

@@ -15,11 +16,13 @@ def __init__(
1516
) -> None:
1617
super().__init__(context)
1718
self.core_lifecycle = core_lifecycle
19+
self._background_tasks: set[asyncio.Task] = set()
1820
self.routes = [
1921
("/cron/jobs", ("GET", self.list_jobs)),
2022
("/cron/jobs", ("POST", self.create_job)),
2123
("/cron/jobs/<job_id>", ("PATCH", self.update_job)),
2224
("/cron/jobs/<job_id>", ("DELETE", self.delete_job)),
25+
("/cron/jobs/<job_id>/run", ("POST", self.run_job_now)),
2326
]
2427
self.register_routes()
2528

@@ -71,16 +74,14 @@ async def create_job(self):
7174
name = payload.get("name") or "active_agent_task"
7275
cron_expression = payload.get("cron_expression")
7376
note = payload.get("note") or payload.get("description") or name
74-
session = payload.get("session")
77+
session = str(payload.get("session") or "").strip()
7578
persona_id = payload.get("persona_id")
7679
provider_id = payload.get("provider_id")
7780
timezone = payload.get("timezone")
7881
enabled = bool(payload.get("enabled", True))
7982
run_once = bool(payload.get("run_once", False))
8083
run_at = payload.get("run_at")
8184

82-
if not session:
83-
return jsonify(Response().error("session is required").__dict__)
8485
if run_once and not run_at:
8586
return jsonify(
8687
Response().error("run_at is required when run_once=true").__dict__
@@ -172,11 +173,10 @@ async def update_job(self, job_id: str):
172173

173174
if "session" in payload:
174175
session = str(payload.get("session") or "").strip()
175-
if not session:
176-
return jsonify(
177-
Response().error("session cannot be empty").__dict__
178-
)
179-
merged_payload["session"] = session
176+
if session:
177+
merged_payload["session"] = session
178+
else:
179+
merged_payload.pop("session", None)
180180

181181
note_updated = False
182182
if "note" in payload:
@@ -281,3 +281,21 @@ async def delete_job(self, job_id: str):
281281
except Exception as e: # noqa: BLE001
282282
logger.error(traceback.format_exc())
283283
return jsonify(Response().error(f"Failed to delete job: {e!s}").__dict__)
284+
285+
async def run_job_now(self, job_id: str):
286+
try:
287+
cron_mgr = self.core_lifecycle.cron_manager
288+
if cron_mgr is None:
289+
return jsonify(
290+
Response().error("Cron manager not initialized").__dict__
291+
)
292+
job = await cron_mgr.db.get_cron_job(job_id)
293+
if not job:
294+
return jsonify(Response().error("Job not found").__dict__)
295+
task = asyncio.create_task(cron_mgr.run_job_now(job_id))
296+
self._background_tasks.add(task)
297+
task.add_done_callback(self._background_tasks.discard)
298+
return jsonify(Response().ok(message="started").__dict__)
299+
except Exception as e: # noqa: BLE001
300+
logger.error(traceback.format_exc())
301+
return jsonify(Response().error(f"Failed to run job: {e!s}").__dict__)

dashboard/src/assets/mdi-subset/materialdesignicons-subset.css

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Auto-generated MDI subset – 268 icons */
1+
/* Auto-generated MDI subset – 271 icons */
22
/* Do not edit manually. Run: pnpm run subset-icons */
33

44
@font-face {
@@ -228,6 +228,10 @@
228228
content: "\F0150";
229229
}
230230

231+
.mdi-clock-time-four-outline::before {
232+
content: "\F144E";
233+
}
234+
231235
.mdi-close::before {
232236
content: "\F0156";
233237
}
@@ -820,6 +824,10 @@
820824
content: "\F040A";
821825
}
822826

827+
.mdi-play-circle-outline::before {
828+
content: "\F040D";
829+
}
830+
823831
.mdi-plus::before {
824832
content: "\F0415";
825833
}
@@ -892,6 +900,10 @@
892900
content: "\F167A";
893901
}
894902

903+
.mdi-send-outline::before {
904+
content: "\F1165";
905+
}
906+
895907
.mdi-server::before {
896908
content: "\F048B";
897909
}
Binary file not shown.
Binary file not shown.

dashboard/src/i18n/locales/en-US/features/cron.json

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
"page": {
77
"title": "Future Task Management",
88
"beta": "Experimental",
9-
"subtitle": "See scheduled tasks for AstrBot. AstrBot will wake up, run them, and deliver the results.",
9+
"subtitle": "AstrBot wakes up at the scheduled time, completes the task, and sends the result back to the target conversation.",
1010
"proactive": {
11+
"link": "Supported platforms",
1112
"supported": "Proactive delivery is available only on the configured platforms below",
1213
"unsupported": "No proactive messaging platforms enabled. Turn them on in Platform settings."
1314
}
@@ -18,9 +19,36 @@
1819
"refresh": "Refresh",
1920
"delete": "Delete",
2021
"cancel": "Cancel",
22+
"close": "Close",
23+
"more": "More actions",
24+
"runNow": "Run now",
2125
"save": "Save",
2226
"submit": "Create"
2327
},
28+
"platformDialog": {
29+
"title": "Supported Platforms",
30+
"description": "Only the following platforms support proactive message delivery. AstrBot can send future task results back only to target conversations on these platforms."
31+
},
32+
"card": {
33+
"onceAt": "One-off · {time}",
34+
"runAt": "Run at: {time}",
35+
"nextRun": "Next run: {time}",
36+
"dailyAt": "Daily at {time}",
37+
"weeklyAt": "Every {day} at {time}",
38+
"monthlyAt": "Monthly on day {day} at {time}",
39+
"everyMinutes": "Every {count} minutes",
40+
"everyHours": "Every {count} hours",
41+
"everyDays": "Every {count} days",
42+
"customCron": "Custom · {cron}",
43+
"noDeliveryTarget": "No delivery target"
44+
},
45+
"filters": {
46+
"search": "Search title and content",
47+
"umo": "Filter by delivery target",
48+
"noUmos": "No UMOs",
49+
"noDeliveryTarget": "No delivery target",
50+
"noMatches": "No matching future tasks."
51+
},
2452
"overview": {
2553
"totalTasks": "Total Tasks",
2654
"totalTasksNote": "Registered future tasks",
@@ -82,12 +110,43 @@
82110
"editTitle": "Edit Task",
83111
"chatHint": "You can ask AstrBot in chat to create future tasks instead of adding them here.",
84112
"runOnce": "One-off task",
85-
"name": "Task name",
86-
"note": "Task description",
87-
"cron": "Cron expression",
113+
"name": "Task name *",
114+
"note": "Task requirements *",
115+
"scheduleMode": "Execution time *",
116+
"scheduleModes": {
117+
"once": "One-off",
118+
"interval": "Interval",
119+
"daily": "Daily",
120+
"weekly": "Weekly",
121+
"monthly": "Monthly",
122+
"cron": "Custom Cron"
123+
},
124+
"intervalEvery": "Every *",
125+
"intervalUnit": "Unit *",
126+
"intervalUnits": {
127+
"minutes": "Minutes",
128+
"hours": "Hours",
129+
"days": "Days"
130+
},
131+
"dailyTime": "Time *",
132+
"weeklyDay": "Weekday *",
133+
"weeklyTime": "Time *",
134+
"monthlyDay": "Day *",
135+
"monthlyTime": "Time *",
136+
"weekdays": {
137+
"sunday": "Sunday",
138+
"monday": "Monday",
139+
"tuesday": "Tuesday",
140+
"wednesday": "Wednesday",
141+
"thursday": "Thursday",
142+
"friday": "Friday",
143+
"saturday": "Saturday"
144+
},
145+
"cron": "Cron expression *",
88146
"cronPlaceholder": "0 9 * * *",
89-
"runAt": "Run at",
90-
"session": "Target session (platform_id:message_type:session_id)",
147+
"runAt": "Run at *",
148+
"session": "Deliver to",
149+
"noUmos": "No sessions available",
91150
"timezone": "Timezone (optional, e.g. Asia/Shanghai)",
92151
"enabled": "Enabled"
93152
},
@@ -97,11 +156,18 @@
97156
"updateFailed": "Failed to update",
98157
"deleteSuccess": "Deleted",
99158
"deleteFailed": "Failed to delete",
100-
"sessionRequired": "Session is required",
101-
"noteRequired": "Description is required",
159+
"nameRequired": "Task name is required",
160+
"sessionRequired": "Delivery target is required",
161+
"noteRequired": "Task requirements are required",
102162
"cronRequired": "Cron expression is required",
103163
"runAtRequired": "Please select run time",
164+
"intervalRequired": "Please enter a valid interval",
165+
"dailyTimeRequired": "Please select the daily run time",
166+
"weeklyTimeRequired": "Please select the weekly day and time",
167+
"monthlyTimeRequired": "Please select the monthly day and time",
104168
"createSuccess": "Created successfully",
105-
"createFailed": "Failed to create"
169+
"createFailed": "Failed to create",
170+
"runStarted": "Started",
171+
"runFailed": "Failed to run"
106172
}
107173
}

0 commit comments

Comments
 (0)