feat: enhance cron job management with delivery target handling and UI improvements

This commit is contained in:
Soulter
2026-06-03 21:41:12 +08:00
parent 5b420c74be
commit f28ae5f73e
6 changed files with 176 additions and 78 deletions

View File

@@ -15,6 +15,7 @@ from astrbot.core.cron.events import CronMessageEvent
from astrbot.core.db import BaseDatabase
from astrbot.core.db.po import CronJob
from astrbot.core.platform.message_session import MessageSession
from astrbot.core.platform.message_type import MessageType
from astrbot.core.provider.entites import ProviderRequest
from astrbot.core.utils.history_saver import persist_agent_history
@@ -254,9 +255,14 @@ class CronJobManager:
async def _run_active_agent_job(self, job: CronJob, start_time: datetime) -> None:
payload = job.payload or {}
session_str = payload.get("session")
if not session_str:
raise ValueError("ActiveAgentCronJob missing session.")
delivery_session_str = str(payload.get("session") or "").strip()
session_str = delivery_session_str or str(
MessageSession(
platform_name="cron",
message_type=MessageType.OTHER_MESSAGE,
session_id=job.job_id,
)
)
note = payload.get("note") or job.description or job.name
extras = {
@@ -271,7 +277,7 @@ class CronJobManager:
"run_at": (
job.payload.get("run_at") if isinstance(job.payload, dict) else None
),
"session": session_str,
"session": delivery_session_str,
},
"cron_payload": payload,
}
@@ -280,6 +286,7 @@ class CronJobManager:
message=note,
session_str=session_str,
extras=extras,
delivery_session_str=delivery_session_str,
)
async def _woke_main_agent(
@@ -288,6 +295,7 @@ class CronJobManager:
message: str,
session_str: str,
extras: dict,
delivery_session_str: str = "",
) -> None:
"""Woke the main agent to handle the cron job message."""
from astrbot.core.astr_main_agent import (
@@ -362,11 +370,12 @@ class CronJobManager:
"Output using same language as previous conversation. "
"After completing your task, summarize and output your actions and results."
)
if not req.func_tool:
req.func_tool = ToolSet()
req.func_tool.add_tool(
self.ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool)
)
if delivery_session_str:
if not req.func_tool:
req.func_tool = ToolSet()
req.func_tool.add_tool(
self.ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool)
)
result = await build_main_agent(
event=cron_event, plugin_context=self.ctx, config=config, req=req

View File

@@ -16,6 +16,7 @@ class CronRoute(Route):
) -> None:
super().__init__(context)
self.core_lifecycle = core_lifecycle
self._background_tasks: set[asyncio.Task] = set()
self.routes = [
("/cron/jobs", ("GET", self.list_jobs)),
("/cron/jobs", ("POST", self.create_job)),
@@ -73,7 +74,7 @@ class CronRoute(Route):
name = payload.get("name") or "active_agent_task"
cron_expression = payload.get("cron_expression")
note = payload.get("note") or payload.get("description") or name
session = payload.get("session")
session = str(payload.get("session") or "").strip()
persona_id = payload.get("persona_id")
provider_id = payload.get("provider_id")
timezone = payload.get("timezone")
@@ -81,8 +82,6 @@ class CronRoute(Route):
run_once = bool(payload.get("run_once", False))
run_at = payload.get("run_at")
if not session:
return jsonify(Response().error("session is required").__dict__)
if run_once and not run_at:
return jsonify(
Response().error("run_at is required when run_once=true").__dict__
@@ -174,11 +173,10 @@ class CronRoute(Route):
if "session" in payload:
session = str(payload.get("session") or "").strip()
if not session:
return jsonify(
Response().error("session cannot be empty").__dict__
)
merged_payload["session"] = session
if session:
merged_payload["session"] = session
else:
merged_payload.pop("session", None)
note_updated = False
if "note" in payload:
@@ -294,7 +292,9 @@ class CronRoute(Route):
job = await cron_mgr.db.get_cron_job(job_id)
if not job:
return jsonify(Response().error("Job not found").__dict__)
asyncio.create_task(cron_mgr.run_job_now(job_id))
task = asyncio.create_task(cron_mgr.run_job_now(job_id))
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
return jsonify(Response().ok(message="started").__dict__)
except Exception as e: # noqa: BLE001
logger.error(traceback.format_exc())

View File

@@ -39,12 +39,14 @@
"everyMinutes": "Every {count} minutes",
"everyHours": "Every {count} hours",
"everyDays": "Every {count} days",
"customCron": "Custom · {cron}"
"customCron": "Custom · {cron}",
"noDeliveryTarget": "No delivery target"
},
"filters": {
"search": "Search title and content",
"umo": "Filter by delivery target",
"noUmos": "No UMOs",
"noDeliveryTarget": "No delivery target",
"noMatches": "No matching future tasks."
},
"overview": {
@@ -108,9 +110,9 @@
"editTitle": "Edit Task",
"chatHint": "You can ask AstrBot in chat to create future tasks instead of adding them here.",
"runOnce": "One-off task",
"name": "Task name",
"note": "Task requirements",
"scheduleMode": "Execution time",
"name": "Task name *",
"note": "Task requirements *",
"scheduleMode": "Execution time *",
"scheduleModes": {
"once": "One-off",
"interval": "Interval",
@@ -119,18 +121,18 @@
"monthly": "Monthly",
"cron": "Custom Cron"
},
"intervalEvery": "Every",
"intervalUnit": "Unit",
"intervalEvery": "Every *",
"intervalUnit": "Unit *",
"intervalUnits": {
"minutes": "Minutes",
"hours": "Hours",
"days": "Days"
},
"dailyTime": "Time",
"weeklyDay": "Weekday",
"weeklyTime": "Time",
"monthlyDay": "Day",
"monthlyTime": "Time",
"dailyTime": "Time *",
"weeklyDay": "Weekday *",
"weeklyTime": "Time *",
"monthlyDay": "Day *",
"monthlyTime": "Time *",
"weekdays": {
"sunday": "Sunday",
"monday": "Monday",
@@ -140,9 +142,9 @@
"friday": "Friday",
"saturday": "Saturday"
},
"cron": "Cron expression",
"cron": "Cron expression *",
"cronPlaceholder": "0 9 * * *",
"runAt": "Run at",
"runAt": "Run at *",
"session": "Deliver to",
"noUmos": "No sessions available",
"timezone": "Timezone (optional, e.g. Asia/Shanghai)",
@@ -159,6 +161,10 @@
"noteRequired": "Task requirements are required",
"cronRequired": "Cron expression is required",
"runAtRequired": "Please select run time",
"intervalRequired": "Please enter a valid interval",
"dailyTimeRequired": "Please select the daily run time",
"weeklyTimeRequired": "Please select the weekly day and time",
"monthlyTimeRequired": "Please select the monthly day and time",
"createSuccess": "Created successfully",
"createFailed": "Failed to create",
"runStarted": "Started",

View File

@@ -39,12 +39,14 @@
"everyMinutes": "Каждые {count} мин.",
"everyHours": "Каждые {count} ч.",
"everyDays": "Каждые {count} дн.",
"customCron": "Вручную · {cron}"
"customCron": "Вручную · {cron}",
"noDeliveryTarget": "Цель доставки не задана"
},
"filters": {
"search": "Поиск по названию и содержанию",
"umo": "Фильтр по цели доставки",
"noUmos": "Нет UMO",
"noDeliveryTarget": "Цель доставки не задана",
"noMatches": "Нет подходящих будущих задач."
},
"overview": {
@@ -108,9 +110,9 @@
"editTitle": "Редактировать задачу",
"chatHint": "Вы можете ставить задачи прямо в чате, AstrBot создаст их автоматически без заполнения этой формы.",
"runOnce": "Разовая задача",
"name": "Имя задачи",
"note": "Требования задачи",
"scheduleMode": "Время выполнения",
"name": "Имя задачи *",
"note": "Требования задачи *",
"scheduleMode": "Время выполнения *",
"scheduleModes": {
"once": "Один раз",
"interval": "Интервал",
@@ -119,18 +121,18 @@
"monthly": "Каждый месяц",
"cron": "Cron вручную"
},
"intervalEvery": "Каждые",
"intervalUnit": "Единица",
"intervalEvery": "Каждые *",
"intervalUnit": "Единица *",
"intervalUnits": {
"minutes": "Минуты",
"hours": "Часы",
"days": "Дни"
},
"dailyTime": "Время",
"weeklyDay": "День недели",
"weeklyTime": "Время",
"monthlyDay": "День",
"monthlyTime": "Время",
"dailyTime": "Время *",
"weeklyDay": "День недели *",
"weeklyTime": "Время *",
"monthlyDay": "День *",
"monthlyTime": "Время *",
"weekdays": {
"sunday": "Воскресенье",
"monday": "Понедельник",
@@ -140,9 +142,9 @@
"friday": "Пятница",
"saturday": "Суббота"
},
"cron": "Cron-выражения",
"cron": "Cron-выражения *",
"cronPlaceholder": "0 9 * * *",
"runAt": "Время запуска",
"runAt": "Время запуска *",
"session": "Доставить в",
"noUmos": "Нет доступных сессий",
"timezone": "Часовой пояс (опционально, напр. Europe/Moscow)",
@@ -159,6 +161,10 @@
"noteRequired": "Заполните требования задачи",
"cronRequired": "Укажите Cron-выражение",
"runAtRequired": "Выберите время запуска",
"intervalRequired": "Укажите корректный интервал",
"dailyTimeRequired": "Выберите ежедневное время запуска",
"weeklyTimeRequired": "Выберите день недели и время запуска",
"monthlyTimeRequired": "Выберите день месяца и время запуска",
"createSuccess": "Задача создана",
"createFailed": "Ошибка создания",
"runStarted": "Запущено",

View File

@@ -39,12 +39,14 @@
"everyMinutes": "每隔 {count} 分钟",
"everyHours": "每隔 {count} 小时",
"everyDays": "每隔 {count} 天",
"customCron": "自定义 · {cron}"
"customCron": "自定义 · {cron}",
"noDeliveryTarget": "未设置投递地"
},
"filters": {
"search": "搜索任务名称和内容",
"umo": "按投递地筛选",
"noUmos": "暂无 UMO",
"noDeliveryTarget": "未设置投递地",
"noMatches": "没有匹配的未来任务。"
},
"overview": {
@@ -108,9 +110,9 @@
"editTitle": "编辑任务",
"chatHint": "你可以直接通过聊天的方式来让 AstrBot 创建未来任务,而不必在此添加。",
"runOnce": "一次性任务",
"name": "任务名称",
"note": "任务需求",
"scheduleMode": "执行时间",
"name": "任务名称 *",
"note": "任务需求 *",
"scheduleMode": "执行时间 *",
"scheduleModes": {
"once": "一次性",
"interval": "间隔",
@@ -119,18 +121,18 @@
"monthly": "每个月",
"cron": "自定义"
},
"intervalEvery": "每隔",
"intervalUnit": "单位",
"intervalEvery": "每隔 *",
"intervalUnit": "单位 *",
"intervalUnits": {
"minutes": "分钟",
"hours": "小时",
"days": "天"
},
"dailyTime": "时间",
"weeklyDay": "星期",
"weeklyTime": "时间",
"monthlyDay": "日期",
"monthlyTime": "时间",
"dailyTime": "时间 *",
"weeklyDay": "星期 *",
"weeklyTime": "时间 *",
"monthlyDay": "日期 *",
"monthlyTime": "时间 *",
"weekdays": {
"sunday": "周日",
"monday": "周一",
@@ -140,9 +142,9 @@
"friday": "周五",
"saturday": "周六"
},
"cron": "Cron 表达式",
"cron": "Cron 表达式 *",
"cronPlaceholder": "0 9 * * *",
"runAt": "执行时间",
"runAt": "执行时间 *",
"session": "投递到",
"noUmos": "暂无可用会话",
"timezone": "时区(可选,如 Asia/Shanghai",
@@ -159,6 +161,10 @@
"noteRequired": "请填写任务需求",
"cronRequired": "请填写 Cron 表达式",
"runAtRequired": "请选择执行时间",
"intervalRequired": "请填写有效的间隔时间",
"dailyTimeRequired": "请选择每天执行的时间",
"weeklyTimeRequired": "请选择每周执行的星期和时间",
"monthlyTimeRequired": "请选择每月执行的日期和时间",
"createSuccess": "创建成功",
"createFailed": "创建失败",
"runStarted": "已开始执行",

View File

@@ -68,6 +68,8 @@
<v-autocomplete
v-model="selectedUmoFilter"
:items="jobUmoFilterOptions"
item-title="label"
item-value="value"
:label="tm('filters.umo')"
prepend-inner-icon="mdi-send-outline"
variant="solo-filled"
@@ -110,7 +112,7 @@
<div class="task-meta text-caption text-medium-emphasis">
<span class="task-meta-item">
<v-icon size="small" class="me-1">mdi-send-outline</v-icon>
{{ item.session || tm("table.notAvailable") }}
{{ deliveryTargetText(item) }}
</span>
<span class="task-meta-item">
<v-icon size="small" class="me-1">
@@ -419,6 +421,7 @@ const createDialog = ref(false);
const creating = ref(false);
const editingJobId = ref("");
const runningJobIds = ref(new Set<string>());
const NO_DELIVERY_TARGET_FILTER = "__astrbot_no_delivery_target__";
type ScheduleMode =
| "once"
| "interval"
@@ -448,22 +451,29 @@ const newJob = ref({
const snackbar = ref({ show: false, message: "", color: "success" });
const jobUmoFilterOptions = computed(() =>
Array.from(
new Set(
jobs.value
.map((job) => String(job.session || job?.payload?.session || "").trim())
.filter(Boolean),
),
).sort((a, b) => a.localeCompare(b)),
);
const jobUmoFilterOptions = computed(() => [
...(jobs.value.some((job) => !getJobSession(job))
? [
{
label: tm("filters.noDeliveryTarget"),
value: NO_DELIVERY_TARGET_FILTER,
},
]
: []),
...Array.from(new Set(jobs.value.map(getJobSession).filter(Boolean)))
.sort((a, b) => a.localeCompare(b))
.map((umo) => ({ label: umo, value: umo })),
]);
const filteredJobs = computed(() => {
const query = taskSearch.value.trim().toLowerCase();
const umo = selectedUmoFilter.value;
return jobs.value.filter((job) => {
const session = String(job.session || job?.payload?.session || "").trim();
if (umo && session !== umo) {
const session = getJobSession(job);
if (umo === NO_DELIVERY_TARGET_FILTER && session) {
return false;
}
if (umo && umo !== NO_DELIVERY_TARGET_FILTER && session !== umo) {
return false;
}
@@ -555,6 +565,14 @@ function taskPreview(item: any): string {
return text.length > 86 ? `${text.slice(0, 86)}...` : text;
}
function getJobSession(job: any): string {
return String(job.session || job?.payload?.session || "").trim();
}
function deliveryTargetText(item: any): string {
return getJobSession(item) || tm("card.noDeliveryTarget");
}
function nextRunText(item: any): string {
if (item.run_once) {
return tm("card.runAt", { time: formatTime(item.run_at) });
@@ -836,7 +854,7 @@ function openEdit(job: any) {
function parseTimeParts(
value: string,
): { hour: number; minute: number } | null {
const match = /^(\d{2}):(\d{2})$/.exec(value || "");
const match = /^(\d{2}):(\d{2})(?::\d{2})?$/.exec(value || "");
if (!match) return null;
const hour = Number(match[1]);
const minute = Number(match[2]);
@@ -1037,19 +1055,72 @@ function validateJobForm(): boolean {
toast(tm("messages.nameRequired"), "warning");
return false;
}
if (!newJob.value.session) {
toast(tm("messages.sessionRequired"), "warning");
return false;
}
if (!newJob.value.note.trim()) {
toast(tm("messages.noteRequired"), "warning");
return false;
}
if (newJob.value.schedule_mode === "once" && !newJob.value.run_at) {
toast(tm("messages.runAtRequired"), "warning");
return false;
return validateScheduleFields();
}
function validateScheduleFields(): boolean {
const mode = newJob.value.schedule_mode;
if (mode === "once") {
if (!newJob.value.run_at) {
toast(tm("messages.runAtRequired"), "warning");
return false;
}
return true;
}
if (newJob.value.schedule_mode !== "once" && !buildCronExpression()) {
if (mode === "interval") {
const value = Number(newJob.value.interval_value);
const validUnit = ["minutes", "hours", "days"].includes(
newJob.value.interval_unit,
);
if (!Number.isInteger(value) || value < 1 || !validUnit) {
toast(tm("messages.intervalRequired"), "warning");
return false;
}
return true;
}
if (mode === "daily") {
if (!parseTimeParts(newJob.value.daily_time)) {
toast(tm("messages.dailyTimeRequired"), "warning");
return false;
}
return true;
}
if (mode === "weekly") {
const weekday = Number(newJob.value.weekly_day);
if (
!parseTimeParts(newJob.value.weekly_time) ||
!Number.isInteger(weekday) ||
weekday < 0 ||
weekday > 6
) {
toast(tm("messages.weeklyTimeRequired"), "warning");
return false;
}
return true;
}
if (mode === "monthly") {
const day = Number(newJob.value.monthly_day);
if (
!parseTimeParts(newJob.value.monthly_time) ||
!Number.isInteger(day) ||
day < 1 ||
day > 31
) {
toast(tm("messages.monthlyTimeRequired"), "warning");
return false;
}
return true;
}
if (!newJob.value.cron_expression.trim()) {
toast(tm("messages.cronRequired"), "warning");
return false;
}