feat: enhance cron job management and update UI terminology
This commit is contained in:
@@ -569,6 +569,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||
)
|
||||
],
|
||||
)
|
||||
logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}")
|
||||
|
||||
# 处理函数调用响应
|
||||
if tool_call_result_blocks:
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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": "平台日志",
|
||||
|
||||
@@ -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 段 Cron,例:0 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 })
|
||||
|
||||
Reference in New Issue
Block a user