feat: enhance cron job management and update UI terminology

This commit is contained in:
Soulter
2026-02-01 15:49:14 +08:00
parent 83288ca43e
commit 4c8c87d3fd
9 changed files with 41 additions and 106 deletions
@@ -569,6 +569,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
)
],
)
logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}")
# 处理函数调用响应
if tool_call_result_blocks:
+15 -3
View File
@@ -3,6 +3,7 @@ import base64
from pydantic import Field
from pydantic.dataclasses import dataclass
import astrbot.core.message.components as Comp
from astrbot.api import logger, sp
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
@@ -183,12 +184,15 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
"type": "string",
"description": "What you want to tell the user.",
},
"image_path": {
"type": "string",
"description": "Optional. Send an image to the user by specifying the file path. Use an absolute path when possible; otherwise, ensure the path is relative to `data/`.",
},
"session": {
"type": "string",
"description": "Optional target session in format platform_id:message_type:session_id. Defaults to current session.",
},
},
"required": ["message"],
}
)
@@ -196,11 +200,19 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> ToolExecResult:
message = str(kwargs.get("message", "")).strip()
image_path = kwargs.get("image_path")
session = kwargs.get("session") or context.context.event.unified_msg_origin
if not message:
if not message and not image_path:
return "error: message is empty."
comps: list[Comp.BaseMessageComponent] = []
if message:
comps.append(Comp.Plain(text=message))
if image_path:
comps.append(Comp.Image.fromFileSystem(path=image_path))
try:
target_session = (
MessageSession.from_str(session)
@@ -212,7 +224,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
await context.context.context.send_message(
target_session,
MessageChain().message(message),
MessageChain(chain=comps),
)
return f"Message sent to session {target_session}"
+1 -1
View File
@@ -159,7 +159,7 @@ class CronJob(TimestampMixin, SQLModel, table=True):
description: str | None = Field(default=None, sa_type=Text)
job_type: str = Field(
max_length=32, nullable=False
) # basic | active_agent | background
) # basic | active_agent
cron_expression: str | None = Field(default=None, max_length=255)
timezone: str | None = Field(default=None, max_length=64)
payload: dict = Field(default_factory=dict, sa_type=JSON)
+9 -8
View File
@@ -62,6 +62,7 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
# Based on openai/codex
return (
"## Skills\n"
"You have many useful skills that can help you accomplish various tasks.\n"
"A skill is a set of local instructions stored in a `SKILL.md` file.\n"
"### Available skills\n"
f"{skills_block}\n"
@@ -69,21 +70,21 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
"\n"
"- Discovery: The list above shows all skills available in this session. Full instructions live in the referenced `SKILL.md`.\n"
"- Trigger rules: Use a skill if the user names it or the task matches its description. Do not carry skills across turns unless re-mentioned\n"
"- Unavailable: If a skill is missing or unreadable, say so and fallback.\n"
"### How to use a skill (progressive disclosure):\n"
" 1) After deciding to use a skill, open its `SKILL.md` and read only what is necessary to follow the workflow.\n"
" 2) Load only directly referenced files, DO NOT bulk-load everything.\n"
" 3) If `scripts/` exist, prefer running or patching them instead of retyping large blocks of code.\n"
" 4) If `assets/` or templates exist, reuse them rather than recreating everything from scratch.\n"
" 0) Mandatory grounding: Before using any skill, you MUST inspect its `SKILL.md` using shell tools"
" (e.g., `cat`, `head`, `sed`, `awk`, `grep`). Do not rely on assumptions or memory.\n"
" 1) Load only directly referenced files, DO NOT bulk-load everything.\n"
" 2) If `scripts/` exist, prefer running or patching them instead of retyping large blocks of code.\n"
" 3) If `assets/` or templates exist, reuse them rather than recreating everything from scratch.\n"
"- Coordination:\n"
" - If multiple skills apply, choose the minimal set that covers the request and state the order in which you will use them.\n"
" - Announce which skill(s) you are using and why (one short line). If you skip an obvious skill, explain why.\n"
" - Prefer to use `astrbot_*` tools to perform skills that need to run scripts.\n"
"- Context hygiene:\n"
" - Keep context small: summarize long sections instead of pasting them, and load extra files only when necessary.\n"
" - Avoid deep reference chasing: unless blocked, open only files that are directly linked from `SKILL.md`.\n"
" - When variants exist (frameworks, providers, domains), select only the relevant reference file(s) and note that choice.\n"
"- Failure handling: If a skill cannot be applied, state the issue and continue with the best alternative."
"- Failure handling: If a skill cannot be applied, state the issue and continue with the best alternative.\n"
"### Example\n"
"When you decided to use a skill, use shell tool to read its `SKILL.md`, e.g., `head -40 skills/code_formatter/SKILL.md`, and you can increase or decrease the number of lines as needed.\n"
)
+1 -1
View File
@@ -62,7 +62,7 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
next_run = job.next_run_time
return (
f"Scheduled future task {job.job_id} ({job.name}) with expression '{cron_expression}'. "
f"Your future agent will wake at: {next_run}"
f"You will be awakened at: {next_run}"
)
+5
View File
@@ -28,6 +28,11 @@ class CronRoute(Route):
for k in ["created_at", "updated_at", "last_run_at", "next_run_time"]:
if isinstance(data.get(k), datetime):
data[k] = data[k].isoformat()
# expose note explicitly for UI (prefer payload.note then description)
payload = data.get("payload") or {}
data["note"] = payload.get("note") or data.get("description") or ""
# status is internal; hide to avoid implying one-time completion for recurring jobs
data.pop("status", None)
return data
async def list_jobs(self):
@@ -8,7 +8,7 @@
"toolUse": "MCP Tools",
"config": "Config",
"chat": "Chat",
"cron": "Cron Jobs",
"cron": "Future Tasks",
"extension": "Extensions",
"conversation": "Conversations",
"sessionManagement": "Custom Rules",
@@ -9,7 +9,7 @@
"extension": "插件",
"config": "配置文件",
"chat": "聊天",
"cron": "定时任务",
"cron": "未来任务",
"conversation": "对话数据",
"sessionManagement": "自定义规则",
"console": "平台日志",
+7 -91
View File
@@ -2,58 +2,21 @@
<div class="cron-page">
<div class="d-flex align-center justify-space-between mb-4">
<div>
<h2 class="text-h5 font-weight-bold">Cron Job 管理</h2>
<div class="text-body-2 text-medium-emphasis">查看创建与管理定时任务ActiveAgent & 后台任务</div>
<h2 class="text-h5 font-weight-bold">未来任务管理</h2>
<div class="text-body-2 text-medium-emphasis">查看 AstrBot 布置的未来任务AstrBot 将会被自动唤醒执行任务然后将结果告知任务布置方</div>
</div>
<div class="d-flex align-center" style="gap: 8px;">
<v-btn variant="tonal" color="primary" :loading="loading" @click="loadJobs">刷新</v-btn>
</div>
</div>
<v-card class="rounded-lg mb-6" variant="flat">
<v-card-text>
<div class="text-subtitle-1 font-weight-bold mb-3">新建主动型 Agent 定时任务</div>
<v-row dense>
<v-col cols="12" md="4">
<v-text-field v-model="form.name" label="任务名称" variant="outlined" density="comfortable" hide-details />
</v-col>
<v-col cols="12" md="4">
<v-text-field v-model="form.cron_expression" label="Cron 表达式" variant="outlined" density="comfortable" placeholder="0 8 * * *" hide-details />
<div class="text-caption text-medium-emphasis mt-1">使用标准 5 Cron0 8 * * * 表示每天 8:00</div>
</v-col>
<v-col cols="12" md="4">
<v-text-field v-model="form.session" label="Session (platform:type:id)" variant="outlined" density="comfortable" placeholder="webchat:friend:SESSION_ID" hide-details />
<div class="text-caption text-medium-emphasis mt-1">从聊天侧栏或 Session 管理中复制 unified_msg_origin</div>
</v-col>
<v-col cols="12">
<v-textarea v-model="form.note" label="给未来 Agent 的说明" variant="outlined" rows="3" auto-grow hide-details />
</v-col>
<v-col cols="12" md="4">
<v-text-field v-model="form.persona_id" label="Persona (可选)" variant="outlined" density="comfortable" hide-details />
</v-col>
<v-col cols="12" md="4">
<v-text-field v-model="form.provider_id" label="Provider ID (可选)" variant="outlined" density="comfortable" hide-details />
</v-col>
<v-col cols="12" md="4">
<v-text-field v-model="form.timezone" label="时区 (可选, 例如 Asia/Shanghai)" variant="outlined" density="comfortable" hide-details />
</v-col>
<v-col cols="12" md="3">
<v-switch v-model="form.enabled" inset color="primary" label="启用" hide-details />
</v-col>
<v-col cols="12" class="d-flex justify-end">
<v-btn color="primary" variant="flat" :loading="saving" @click="createJob">创建任务</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card class="rounded-lg" variant="flat">
<v-card-text>
<div class="d-flex align-center justify-space-between mb-3">
<div class="text-subtitle-1 font-weight-bold">已注册任务</div>
</div>
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">暂无定时任务</v-alert>
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">暂无任务</v-alert>
<v-data-table
:items="jobs"
@@ -75,9 +38,8 @@
<div class="text-caption text-medium-emphasis">{{ item.timezone || 'local' }}</div>
</template>
<template #item.next_run_time="{ item }">{{ formatTime(item.next_run_time) }}</template>
<template #item.status="{ item }">
<v-chip :color="statusColor(item.status)" size="small" variant="flat">{{ item.status }}</v-chip>
</template>
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
<template #item.note="{ item }">{{ item.note || '—' }}</template>
<template #item.actions="{ item }">
<div class="d-flex" style="gap: 8px;">
<v-switch
@@ -106,20 +68,8 @@ import { onMounted, ref } from 'vue'
import axios from 'axios'
const loading = ref(false)
const saving = ref(false)
const jobs = ref<any[]>([])
const form = ref({
name: 'active_agent_task',
cron_expression: '',
session: '',
note: '',
persona_id: '',
provider_id: '',
timezone: '',
enabled: true
})
const snackbar = ref({ show: false, message: '', color: 'success' })
const headers = [
@@ -127,7 +77,8 @@ const headers = [
{ title: '类型', key: 'type', width: 110 },
{ title: 'Cron', key: 'cron_expression', minWidth: 160 },
{ title: '下一次执行', key: 'next_run_time', minWidth: 160 },
{ title: '状态', key: 'status', width: 120 },
{ title: '最近执行', key: 'last_run_at', minWidth: 160 },
{ title: '说明', key: 'note', minWidth: 220 },
{ title: '操作', key: 'actions', width: 160, sortable: false }
]
@@ -144,19 +95,6 @@ function formatTime(val: any): string {
}
}
function statusColor(status: string) {
switch ((status || '').toLowerCase()) {
case 'running':
return 'blue'
case 'failed':
return 'error'
case 'completed':
return 'success'
default:
return 'secondary'
}
}
async function loadJobs() {
loading.value = true
try {
@@ -173,28 +111,6 @@ async function loadJobs() {
}
}
async function createJob() {
if (!form.value.cron_expression || !form.value.session || !form.value.note) {
toast('请填写 cron、session 和说明', 'warning')
return
}
saving.value = true
try {
const payload = { ...form.value, job_type: 'active_agent' }
const res = await axios.post('/api/cron/jobs', payload)
if (res.data.status === 'ok') {
toast('创建成功')
await loadJobs()
} else {
toast(res.data.message || '创建失败', 'error')
}
} catch (e: any) {
toast(e?.response?.data?.message || '创建失败', 'error')
} finally {
saving.value = false
}
}
async function toggleJob(job: any) {
try {
const res = await axios.patch(`/api/cron/jobs/${job.job_id}`, { enabled: job.enabled })