Compare commits

...

5 Commits

10 changed files with 1599 additions and 648 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
@@ -200,9 +201,18 @@ class CronJobManager:
return None
return aps_job.next_run_time.astimezone(timezone.utc)
async def _run_job(self, job_id: str) -> None:
async def run_job_now(self, job_id: str) -> None:
await self._run_job(job_id, ignore_enabled=True, delete_run_once=False)
async def _run_job(
self,
job_id: str,
*,
ignore_enabled: bool = False,
delete_run_once: bool = True,
) -> None:
job = await self.db.get_cron_job(job_id)
if not job or not job.enabled:
if not job or (not job.enabled and not ignore_enabled):
return
start_time = datetime.now(timezone.utc)
await self.db.update_cron_job(
@@ -230,7 +240,7 @@ class CronJobManager:
last_error=last_error,
next_run_time=next_run,
)
if job.run_once:
if job.run_once and delete_run_once:
# one-shot: remove after execution regardless of success
await self.delete_job(job_id)
@@ -245,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 = {
@@ -262,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,
}
@@ -271,6 +286,7 @@ class CronJobManager:
message=note,
session_str=session_str,
extras=extras,
delivery_session_str=delivery_session_str,
)
async def _woke_main_agent(
@@ -279,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 (
@@ -353,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

@@ -1,3 +1,4 @@
import asyncio
import traceback
from datetime import datetime, timezone
@@ -15,11 +16,13 @@ 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)),
("/cron/jobs/<job_id>", ("PATCH", self.update_job)),
("/cron/jobs/<job_id>", ("DELETE", self.delete_job)),
("/cron/jobs/<job_id>/run", ("POST", self.run_job_now)),
]
self.register_routes()
@@ -71,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")
@@ -79,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__
@@ -172,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:
@@ -281,3 +281,21 @@ class CronRoute(Route):
except Exception as e: # noqa: BLE001
logger.error(traceback.format_exc())
return jsonify(Response().error(f"Failed to delete job: {e!s}").__dict__)
async def run_job_now(self, job_id: str):
try:
cron_mgr = self.core_lifecycle.cron_manager
if cron_mgr is None:
return jsonify(
Response().error("Cron manager not initialized").__dict__
)
job = await cron_mgr.db.get_cron_job(job_id)
if not job:
return jsonify(Response().error("Job not found").__dict__)
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())
return jsonify(Response().error(f"Failed to run job: {e!s}").__dict__)

View File

@@ -1,4 +1,4 @@
/* Auto-generated MDI subset 268 icons */
/* Auto-generated MDI subset 271 icons */
/* Do not edit manually. Run: pnpm run subset-icons */
@font-face {
@@ -228,6 +228,10 @@
content: "\F0150";
}
.mdi-clock-time-four-outline::before {
content: "\F144E";
}
.mdi-close::before {
content: "\F0156";
}
@@ -820,6 +824,10 @@
content: "\F040A";
}
.mdi-play-circle-outline::before {
content: "\F040D";
}
.mdi-plus::before {
content: "\F0415";
}
@@ -892,6 +900,10 @@
content: "\F167A";
}
.mdi-send-outline::before {
content: "\F1165";
}
.mdi-server::before {
content: "\F048B";
}

View File

@@ -6,8 +6,9 @@
"page": {
"title": "Future Task Management",
"beta": "Experimental",
"subtitle": "See scheduled tasks for AstrBot. AstrBot will wake up, run them, and deliver the results.",
"subtitle": "AstrBot wakes up at the scheduled time, completes the task, and sends the result back to the target conversation.",
"proactive": {
"link": "Supported platforms",
"supported": "Proactive delivery is available only on the configured platforms below",
"unsupported": "No proactive messaging platforms enabled. Turn them on in Platform settings."
}
@@ -18,9 +19,36 @@
"refresh": "Refresh",
"delete": "Delete",
"cancel": "Cancel",
"close": "Close",
"more": "More actions",
"runNow": "Run now",
"save": "Save",
"submit": "Create"
},
"platformDialog": {
"title": "Supported Platforms",
"description": "Only the following platforms support proactive message delivery. AstrBot can send future task results back only to target conversations on these platforms."
},
"card": {
"onceAt": "One-off · {time}",
"runAt": "Run at: {time}",
"nextRun": "Next run: {time}",
"dailyAt": "Daily at {time}",
"weeklyAt": "Every {day} at {time}",
"monthlyAt": "Monthly on day {day} at {time}",
"everyMinutes": "Every {count} minutes",
"everyHours": "Every {count} hours",
"everyDays": "Every {count} days",
"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": {
"totalTasks": "Total Tasks",
"totalTasksNote": "Registered future tasks",
@@ -82,12 +110,43 @@
"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 description",
"cron": "Cron expression",
"name": "Task name *",
"note": "Task requirements *",
"scheduleMode": "Execution time *",
"scheduleModes": {
"once": "One-off",
"interval": "Interval",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly",
"cron": "Custom Cron"
},
"intervalEvery": "Every *",
"intervalUnit": "Unit *",
"intervalUnits": {
"minutes": "Minutes",
"hours": "Hours",
"days": "Days"
},
"dailyTime": "Time *",
"weeklyDay": "Weekday *",
"weeklyTime": "Time *",
"monthlyDay": "Day *",
"monthlyTime": "Time *",
"weekdays": {
"sunday": "Sunday",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday"
},
"cron": "Cron expression *",
"cronPlaceholder": "0 9 * * *",
"runAt": "Run at",
"session": "Target session (platform_id:message_type:session_id)",
"runAt": "Run at *",
"session": "Deliver to",
"noUmos": "No sessions available",
"timezone": "Timezone (optional, e.g. Asia/Shanghai)",
"enabled": "Enabled"
},
@@ -97,11 +156,18 @@
"updateFailed": "Failed to update",
"deleteSuccess": "Deleted",
"deleteFailed": "Failed to delete",
"sessionRequired": "Session is required",
"noteRequired": "Description is required",
"nameRequired": "Task name is required",
"sessionRequired": "Delivery target is required",
"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"
"createFailed": "Failed to create",
"runStarted": "Started",
"runFailed": "Failed to run"
}
}

View File

@@ -1,107 +1,173 @@
{
"header": {
"eyebrow": "Automation",
"live": "Живая синхронизация"
},
"page": {
"title": "Запланированные задачи",
"beta": "Экспериментальные функции",
"subtitle": "Управление будущими задачами AstrBot. Бот автоматически проснется, выполнит задачу и отправит результат. Требуется включить «Проактивные способности» в конфигурации.",
"proactive": {
"supported": "Отправка результатов поддерживается только на указанных ниже настроенных платформах",
"unsupported": "Нет платформ, поддерживающих проактивные сообщения. Включите их в настройках платформ."
}
},
"actions": {
"create": "Новая задача",
"edit": "Изменить",
"refresh": "Обновить",
"delete": "Удалить",
"cancel": "Отмена",
"save": "Сохранить",
"submit": "Создать"
},
"overview": {
"totalTasks": "Всего задач",
"totalTasksNote": "Все зарегистрированные будущие задачи",
"enabledTasks": "Активные задачи",
"enabledTasksNote": "Задачи, которые будут автоматически выполнены",
"oneOffTasks": "Разовые задачи",
"recurringTasksNote": "Повторяющихся задач: {count}",
"proactivePlatforms": "Проактивные платформы",
"proactivePlatformsNote": "Платформы, способные отправлять результат сами"
},
"section": {
"registered": {
"title": "Список задач",
"subtitle": "Просматривайте зарегистрированные задачи, время запуска и состояние"
},
"delivery": {
"title": "Статус доставки",
"subtitle": "После выполнения задачи результат будет отправлен обратно через поддерживаемые платформы",
"support": "Проактивная доставка",
"available": "Доступна",
"unavailable": "Недоступна",
"enabledPlatforms": "Включенные платформы"
},
"quickCreate": {
"title": "Быстрое создание",
"runMode": "Режим задачи",
"target": "Целевой контекст"
},
"platforms": {
"title": "Поддерживаемые платформы"
}
},
"table": {
"title": "Список задач",
"subtitle": "Отслеживайте cron, целевую сессию, историю запусков и состояние",
"empty": "Задач пока нет.",
"headers": {
"name": "Имя",
"type": "Тип",
"cron": "Cron",
"session": "ID сессии",
"nextRun": "Следующий запуск",
"lastRun": "Последний запуск",
"note": "Описание",
"actions": "Действия"
},
"type": {
"once": "Разовая",
"recurring": "Повторяющаяся",
"activeAgent": "Активный агент",
"workflow": "Рабочий процесс",
"unknown": "{type}"
},
"timezoneLocal": "Местное время",
"notAvailable": "—"
},
"form": {
"title": "Создать задачу",
"editTitle": "Редактировать задачу",
"chatHint": "Вы можете ставить задачи прямо в чате, AstrBot создаст их автоматически без заполнения этой формы.",
"runOnce": "Разовая задача",
"name": "Имя задачи",
"note": "Описание",
"cron": "Cron-выражения",
"cronPlaceholder": "0 9 * * *",
"runAt": "Время запуска",
"session": "Целевая сессия (platform_id:message_type:session_id)",
"timezone": "Часовой пояс (опционально, напр. Europe/Moscow)",
"enabled": "Включено"
},
"messages": {
"loadFailed": "Ошибка загрузки задач",
"updateSuccess": "Задача обновлена",
"updateFailed": "Ошибка обновления",
"deleteSuccess": "Удалено",
"deleteFailed": "Ошибка удаления",
"sessionRequired": "Укажите сессию",
"noteRequired": "Заполните описание",
"cronRequired": "Укажите Cron-выражение",
"runAtRequired": "Выберите время запуска",
"createSuccess": "Задача создана",
"createFailed": "Ошибка создания"
"header": {
"eyebrow": "Automation",
"live": "Живая синхронизация"
},
"page": {
"title": "Запланированные задачи",
"beta": "Экспериментальные функции",
"subtitle": "AstrBot проснется в заданное время, выполнит задачу и отправит результат в целевую беседу.",
"proactive": {
"link": "Поддерживаемые платформы",
"supported": "Отправка результатов поддерживается только на указанных ниже настроенных платформах",
"unsupported": "Нет платформ, поддерживающих проактивные сообщения. Включите их в настройках платформ."
}
},
"actions": {
"create": "Новая задача",
"edit": "Изменить",
"refresh": "Обновить",
"delete": "Удалить",
"cancel": "Отмена",
"close": "Закрыть",
"more": "Другие действия",
"runNow": "Запустить сейчас",
"save": "Сохранить",
"submit": "Создать"
},
"platformDialog": {
"title": "Поддерживаемые платформы",
"description": "Только следующие платформы поддерживают проактивную отправку сообщений. AstrBot сможет отправить результат будущей задачи только в целевые беседы на этих платформах."
},
"card": {
"onceAt": "Один раз · {time}",
"runAt": "Время запуска: {time}",
"nextRun": "Следующий запуск: {time}",
"dailyAt": "Каждый день в {time}",
"weeklyAt": "Каждый {day} в {time}",
"monthlyAt": "Каждый месяц, день {day}, {time}",
"everyMinutes": "Каждые {count} мин.",
"everyHours": "Каждые {count} ч.",
"everyDays": "Каждые {count} дн.",
"customCron": "Вручную · {cron}",
"noDeliveryTarget": "Цель доставки не задана"
},
"filters": {
"search": "Поиск по названию и содержанию",
"umo": "Фильтр по цели доставки",
"noUmos": "Нет UMO",
"noDeliveryTarget": "Цель доставки не задана",
"noMatches": "Нет подходящих будущих задач."
},
"overview": {
"totalTasks": "Всего задач",
"totalTasksNote": "Все зарегистрированные будущие задачи",
"enabledTasks": "Активные задачи",
"enabledTasksNote": "Задачи, которые будут автоматически выполнены",
"oneOffTasks": "Разовые задачи",
"recurringTasksNote": "Повторяющихся задач: {count}",
"proactivePlatforms": "Проактивные платформы",
"proactivePlatformsNote": "Платформы, способные отправлять результат сами"
},
"section": {
"registered": {
"title": "Список задач",
"subtitle": "Просматривайте зарегистрированные задачи, время запуска и состояние"
},
"delivery": {
"title": "Статус доставки",
"subtitle": "После выполнения задачи результат будет отправлен обратно через поддерживаемые платформы",
"support": "Проактивная доставка",
"available": "Доступна",
"unavailable": "Недоступна",
"enabledPlatforms": "Включенные платформы"
},
"quickCreate": {
"title": "Быстрое создание",
"runMode": "Режим задачи",
"target": "Целевой контекст"
},
"platforms": {
"title": "Поддерживаемые платформы"
}
},
"table": {
"title": "Список задач",
"subtitle": "Отслеживайте cron, целевую сессию, историю запусков и состояние",
"empty": "Задач пока нет.",
"headers": {
"name": "Имя",
"type": "Тип",
"cron": "Cron",
"session": "ID сессии",
"nextRun": "Следующий запуск",
"lastRun": "Последний запуск",
"note": "Описание",
"actions": "Действия"
},
"type": {
"once": "Разовая",
"recurring": "Повторяющаяся",
"activeAgent": "Активный агент",
"workflow": "Рабочий процесс",
"unknown": "{type}"
},
"timezoneLocal": "Местное время",
"notAvailable": "—"
},
"form": {
"title": "Создать задачу",
"editTitle": "Редактировать задачу",
"chatHint": "Вы можете ставить задачи прямо в чате, AstrBot создаст их автоматически без заполнения этой формы.",
"runOnce": "Разовая задача",
"name": "Имя задачи *",
"note": "Требования задачи *",
"scheduleMode": "Время выполнения *",
"scheduleModes": {
"once": "Один раз",
"interval": "Интервал",
"daily": "Каждый день",
"weekly": "Каждую неделю",
"monthly": "Каждый месяц",
"cron": "Cron вручную"
},
"intervalEvery": "Каждые *",
"intervalUnit": "Единица *",
"intervalUnits": {
"minutes": "Минуты",
"hours": "Часы",
"days": "Дни"
},
"dailyTime": "Время *",
"weeklyDay": "День недели *",
"weeklyTime": "Время *",
"monthlyDay": "День *",
"monthlyTime": "Время *",
"weekdays": {
"sunday": "Воскресенье",
"monday": "Понедельник",
"tuesday": "Вторник",
"wednesday": "Среда",
"thursday": "Четверг",
"friday": "Пятница",
"saturday": "Суббота"
},
"cron": "Cron-выражения *",
"cronPlaceholder": "0 9 * * *",
"runAt": "Время запуска *",
"session": "Доставить в",
"noUmos": "Нет доступных сессий",
"timezone": "Часовой пояс (опционально, напр. Europe/Moscow)",
"enabled": "Включено"
},
"messages": {
"loadFailed": "Ошибка загрузки задач",
"updateSuccess": "Задача обновлена",
"updateFailed": "Ошибка обновления",
"deleteSuccess": "Удалено",
"deleteFailed": "Ошибка удаления",
"nameRequired": "Укажите имя задачи",
"sessionRequired": "Укажите цель доставки",
"noteRequired": "Заполните требования задачи",
"cronRequired": "Укажите Cron-выражение",
"runAtRequired": "Выберите время запуска",
"intervalRequired": "Укажите корректный интервал",
"dailyTimeRequired": "Выберите ежедневное время запуска",
"weeklyTimeRequired": "Выберите день недели и время запуска",
"monthlyTimeRequired": "Выберите день месяца и время запуска",
"createSuccess": "Задача создана",
"createFailed": "Ошибка создания",
"runStarted": "Запущено",
"runFailed": "Ошибка запуска"
}
}

View File

@@ -4,10 +4,11 @@
"live": "实时同步"
},
"page": {
"title": "未来任务管理",
"title": "未来任务",
"beta": "实验性",
"subtitle": "AstrBot 可以被自动唤醒然后执行任务,并将结果告知任务布置方。需要先在配置文件中启用“主动型能力”。",
"subtitle": "AstrBot 会在设定时间自动唤醒,完成任务后把结果投递回目标会话。",
"proactive": {
"link": "支持的消息平台",
"supported": "主动发送结果仅支持以下您已配置的平台",
"unsupported": "暂无支持主动消息的平台,请在平台设置中开启。"
}
@@ -18,9 +19,36 @@
"refresh": "刷新",
"delete": "删除",
"cancel": "取消",
"close": "关闭",
"more": "更多操作",
"runNow": "立即执行",
"save": "保存",
"submit": "创建"
},
"platformDialog": {
"title": "支持的消息平台",
"description": "只有以下平台支持主动推送消息。未来任务完成后AstrBot 才能把结果投递回这些平台的目标会话。"
},
"card": {
"onceAt": "一次性 · {time}",
"runAt": "执行时间:{time}",
"nextRun": "下次执行:{time}",
"dailyAt": "每天 {time}",
"weeklyAt": "每周{day} {time}",
"monthlyAt": "每月 {day} 日 {time}",
"everyMinutes": "每隔 {count} 分钟",
"everyHours": "每隔 {count} 小时",
"everyDays": "每隔 {count} 天",
"customCron": "自定义 · {cron}",
"noDeliveryTarget": "未设置投递地"
},
"filters": {
"search": "搜索任务名称和内容",
"umo": "按投递地筛选",
"noUmos": "暂无 UMO",
"noDeliveryTarget": "未设置投递地",
"noMatches": "没有匹配的未来任务。"
},
"overview": {
"totalTasks": "任务总数",
"totalTasksNote": "当前已注册的未来任务",
@@ -82,12 +110,43 @@
"editTitle": "编辑任务",
"chatHint": "你可以直接通过聊天的方式来让 AstrBot 创建未来任务,而不必在此添加。",
"runOnce": "一次性任务",
"name": "任务名称",
"note": "任务说明",
"cron": "Cron 表达式",
"name": "任务名称 *",
"note": "任务需求 *",
"scheduleMode": "执行时间 *",
"scheduleModes": {
"once": "一次性",
"interval": "间隔",
"daily": "每天",
"weekly": "每周",
"monthly": "每个月",
"cron": "自定义"
},
"intervalEvery": "每隔 *",
"intervalUnit": "单位 *",
"intervalUnits": {
"minutes": "分钟",
"hours": "小时",
"days": "天"
},
"dailyTime": "时间 *",
"weeklyDay": "星期 *",
"weeklyTime": "时间 *",
"monthlyDay": "日期 *",
"monthlyTime": "时间 *",
"weekdays": {
"sunday": "周日",
"monday": "周一",
"tuesday": "周二",
"wednesday": "周三",
"thursday": "周四",
"friday": "周五",
"saturday": "周六"
},
"cron": "Cron 表达式 *",
"cronPlaceholder": "0 9 * * *",
"runAt": "执行时间",
"session": "目标 session (platform_id:message_type:session_id)",
"runAt": "执行时间 *",
"session": "投递到(可选)",
"noUmos": "暂无可用会话",
"timezone": "时区(可选,如 Asia/Shanghai",
"enabled": "启用"
},
@@ -97,11 +156,18 @@
"updateFailed": "更新失败",
"deleteSuccess": "已删除",
"deleteFailed": "删除失败",
"sessionRequired": "请填写 session",
"noteRequired": "请填写说明",
"nameRequired": "请填写任务名称",
"sessionRequired": "请选择投递目标",
"noteRequired": "请填写任务需求",
"cronRequired": "请填写 Cron 表达式",
"runAtRequired": "请选择执行时间",
"intervalRequired": "请填写有效的间隔时间",
"dailyTimeRequired": "请选择每天执行的时间",
"weeklyTimeRequired": "请选择每周执行的星期和时间",
"monthlyTimeRequired": "请选择每月执行的日期和时间",
"createSuccess": "创建成功",
"createFailed": "创建失败"
"createFailed": "创建失败",
"runStarted": "已开始执行",
"runFailed": "执行失败"
}
}

View File

@@ -27,7 +27,7 @@ const PurpleTheme: ThemeTypes = {
borderLight: '#d0d0d0',
border: '#d0d0d0',
inputBorder: '#787878',
containerBg: '#f9fafcf4',
containerBg: '#fffffff4',
surface: '#fff',
'on-surface-variant': '#fff',
facebook: '#4267b2',

File diff suppressed because it is too large Load Diff