mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 09:50:31 +08:00
feat: enhance cron job management with delivery target handling and UI improvements
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Запущено",
|
||||
|
||||
@@ -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": "已开始执行",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user