fix: heartbeat 500 error, timezone to Beijing, tasks guide collapse

- fix(api): heartbeat endpoint no longer requires request body, fix spa_fallback
  404 handler causing 500 on API routes, add db.refresh() in agent update/heartbeat
- feat(time): unify all frontend time display to Beijing time (Asia/Shanghai)
  via new beijing() utility; backend alert messages also use BJT
- feat(ui): collapse AI agent instruction panel on Tasks page by default
- chore: add backend/app/utils.py and frontend/src/utils/time.js
This commit is contained in:
Steven
2026-06-15 03:29:07 +08:00
parent cd41e59afc
commit 6f64eecb43
12 changed files with 445 additions and 130 deletions
+25
View File
@@ -0,0 +1,25 @@
/**
* 时间工具 — 北京时间 (Asia/Shanghai, UTC+8)
*
* 后端存储的日期时间均为 UTCnaive datetime),
* 前端统一转换为北京时间展示。
*/
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
dayjs.extend(utc)
dayjs.extend(timezone)
const TZ = 'Asia/Shanghai'
/**
* 将 UTC 时间戳格式化为北京时间字符串
* @param {string|Date|null} ts ISO 时间字符串或 Date 对象
* @param {string} fmt dayjs 格式模板,默认 'MM-DD HH:mm'
* @returns {string} 格式化后的北京时间
*/
export function beijing(ts, fmt = 'MM-DD HH:mm') {
if (!ts) return '—'
return dayjs.utc(ts).tz(TZ).format(fmt)
}
+2 -2
View File
@@ -51,7 +51,7 @@
</span>
</td>
<td class="cell-mono">{{ a.task_count }}</td>
<td class="cell-mono">{{ a.last_heartbeat_at ? dayjs(a.last_heartbeat_at).format('MM-DD HH:mm') : '—' }}</td>
<td class="cell-mono">{{ a.last_heartbeat_at ? beijing(a.last_heartbeat_at) : '—' }}</td>
<td>
<code class="cell-key">{{ a.api_key.substring(0, 16) }}...</code>
<button class="btn-icon" @click="copyKey(a.api_key)" title="复制 API Key">
@@ -74,7 +74,7 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import dayjs from 'dayjs'
import { beijing } from '../utils/time.js'
import { listAgents, getSystemConfig } from '../api/index.js'
const agents = ref([])
+2 -2
View File
@@ -20,7 +20,7 @@
{{ a.alert_type === 'missed_run' ? '未按时执行' : '执行失败' }}
</span>
<span class="alert-task">{{ a.task_name || '未知任务' }}</span>
<span class="alert-time">{{ dayjs(a.created_at).format('MM-DD HH:mm') }}</span>
<span class="alert-time">{{ beijing(a.created_at) }}</span>
</div>
<div class="alert-msg">{{ a.message }}</div>
<div class="alert-bottom">
@@ -42,7 +42,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import dayjs from 'dayjs'
import { beijing } from '../utils/time.js'
import { listAlerts, acknowledgeAlert } from '../api/index.js'
const alerts = ref([])
+117 -29
View File
@@ -125,7 +125,7 @@
{{ t.status === 'active' ? '运行中' : t.status === 'paused' ? '已暂停' : '已停止' }}
</span>
</td>
<td class="cell-mono">{{ t.last_run_at ? dayjs(t.last_run_at).format('MM-DD HH:mm') : '—' }}</td>
<td class="cell-mono">{{ t.last_run_at ? beijing(t.last_run_at) : '—' }}</td>
<td>
<span v-if="t.last_run_result" class="tag" :class="t.last_run_result === 'success' ? 'tag-green' : 'tag-red'">
{{ t.last_run_result === 'success' ? '成功' : '失败' }}
@@ -149,7 +149,7 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import dayjs from 'dayjs'
import { beijing } from '../utils/time.js'
import { getDashboardSummary, getDashboardTasks, getSystemConfig } from '../api/index.js'
const summary = ref({
@@ -167,62 +167,150 @@ const baseUrl = ref(window.location.origin)
const curlScript = computed(() => {
const b = baseUrl.value
return `# AI Agent 执行以下命令注册自己:
return `# TaskPulse 接入指引 — 请 AI Agent 执行以下步骤
你正在被接入 TaskPulse(定时任务监控系统),完成后你将拥有统一的运行状态看板和超时告警能力。请按步骤操作,无需用户额外干预。
## 第 1 步:注册你自己
向 TaskPulse 注册你的 Agent 身份,系统会返回专属 API Key:
curl -X POST ${b}/api/agents \\
-H "Content-Type: application/json" \\
-d '{"name": "my-agent", "description": "我的 AI Agent"}'
-d '{"name": "<你的名字>", "description": "<你的职责描述>"}'
# 保存返回的 api_key,后续汇报用
响应中"api_key"字段即你的凭证,请妥善保存,后续所有请求都需要通过
Authorization: Bearer <api_key> 鉴权。
# 注册定时任务(替换 YOUR_API_KEY):
curl -X POST "${b}/api/tasks?agent_id=<ID>" \\
## 第 2 步:注册定时任务
逐一注册你管理的每一个定时任务,描述清楚执行周期和用途:
curl -X POST "${b}/api/tasks?agent_id=<AGENT_ID>" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <API_KEY>" \\
-d '{"name": "任务名称", "cron_expression": "*/5 * * * *", "grace_period": 300}'
如果你有多个任务,推荐使用批量注册接口,一次完成:
curl -X POST ${b}/api/agents/register-with-tasks \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <YOUR_API_KEY>" \\
-d '{
"name": "data-sync",
"cron_expression": "*/5 * * * *",
"grace_period": 300
"name": "<你的名字>",
"tasks": [
{"name": "sync-data", "cron_expression": "*/5 * * * *"},
{"name": "daily-report", "cron_expression": "0 9 * * *"}
]
}'
# 执行后汇报
## 第 3 步:修改你的代码 — 每次执行后主动汇报
这是最关键的一步。修改你的任务执行逻辑,在每次执行完毕后(无论成功还是失败)主动向 TaskPulse 汇报结果,这样看板才能展示实时状态。
在你的任务执行代码末尾,加上以下逻辑:
# 任务执行成功后立即调用
curl -X POST "${b}/api/tasks/<TASK_ID>/executions" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <YOUR_API_KEY>" \\
-d '{
"status": "success",
"duration_ms": 1500,
"log": "任务执行完毕"
}'`
-H "Authorization: Bearer <API_KEY>" \\
-d '{"status": "success", "duration_ms": <耗时毫秒>, "log": "<执行日志>"}'
# 任务执行失败时调用
curl -X POST "${b}/api/tasks/<TASK_ID>/executions" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <API_KEY>" \\
-d '{"status": "failed", "duration_ms": <耗时>, "log": "<日志>", "error_message": "<错误信息>"}'
## 第 4 步:保持活跃
定期发送心跳,让系统知道你仍在运行:
curl -X POST ${b}/api/agents/<AGENT_ID>/heartbeat \\
-H "Authorization: Bearer <API_KEY>"
---
完成以上步骤后,前往 TaskPulse 看板(本页面)查看你的运行状态和执行历史。如配置了通知渠道,任务超时未执行时系统会自动告警。`
})
const pythonScript = computed(() => {
const b = baseUrl.value
return `import requests
return `# TaskPulse 接入指引 — 请 AI Agent 执行以下步骤
你正在被接入 TaskPulse(定时任务监控系统),完成后你将拥有统一的运行状态看板和超时告警能力。请按步骤操作,无需用户额外干预。
## 第 1 步:注册你自己
向 TaskPulse 注册你的 Agent 身份,系统会返回专属 API Key:
import requests
BASE = "${b}"
# 1. AI Agent 注册自己
resp = requests.post(f"{BASE}/api/agents", json={
"name": "my-agent",
"description": "我的 AI Agent"
"name": "<你的名字>",
"description": "<你的职责描述>"
})
agent = resp.json()
API_KEY = agent["api_key"] # 保存此 Key
API_KEY = agent["api_key"] # 保存此 Key,后续所有请求通过 Bearer 鉴权
AGENT_ID = agent["id"]
# 2. 注册定时任务
## 第 2 步:注册定时任务
逐一注册你管理的每一个定时任务,描述清楚执行周期和用途:
resp = requests.post(f"{BASE}/api/tasks",
params={"agent_id": AGENT_ID},
headers={"Authorization": f"Bearer {API_KEY}"},
json={"name": "data-sync", "cron_expression": "*/5 * * * *"}
json={"name": "sync-data", "cron_expression": "*/5 * * * *", "grace_period": 300}
)
task = resp.json()
TASK_ID = task["id"]
# 3. 每次执行后汇报
requests.post(f"{BASE}/api/tasks/{TASK_ID}/executions",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"status": "success", "duration_ms": 1500, "log": "done"})`
如果你有多个任务,推荐使用批量注册接口,一次完成:
resp = requests.post(f"{BASE}/api/agents/register-with-tasks",
json={
"name": "<你的名字>",
"tasks": [
{"name": "sync-data", "cron_expression": "*/5 * * * *"},
{"name": "daily-report", "cron_expression": "0 9 * * *"}
]
}
)
## 第 3 步:修改你的代码 — 每次执行后主动汇报
这是最关键的一步。修改你的任务执行逻辑,在每次执行完毕后(无论成功还是失败)主动向 TaskPulse 汇报结果,这样看板才能展示实时状态。
在你的任务执行代码中,加上以下逻辑:
import time
def run_with_reporting(task_func, task_id, api_key):
try:
start = time.time()
result = task_func() # 你的任务逻辑
elapsed = int((time.time() - start) * 1000)
requests.post(f"{BASE}/api/tasks/{task_id}/executions",
headers={"Authorization": f"Bearer {api_key}"},
json={"status": "success", "duration_ms": elapsed, "log": str(result)})
except Exception as e:
requests.post(f"{BASE}/api/tasks/{task_id}/executions",
headers={"Authorization": f"Bearer {api_key}"},
json={"status": "failed", "duration_ms": 0, "error_message": str(e)})
# 使用示例 — 包裹你现有的任务函数:
run_with_reporting(my_task_function, TASK_ID, API_KEY)
## 第 4 步:保持活跃
定期发送心跳,让系统知道你仍在运行:
requests.post(f"{BASE}/api/agents/{AGENT_ID}/heartbeat",
headers={"Authorization": f"Bearer {API_KEY}"})
---
完成以上步骤后,前往 TaskPulse 看板(本页面)查看你的运行状态和执行历史。如配置了通知渠道,任务超时未执行时系统会自动告警。`
})
onMounted(async () => {
+4 -4
View File
@@ -16,7 +16,7 @@
<div class="detail-item"><span class="detail-label">Agent</span><span class="detail-value">{{ task.agent_name }}</span></div>
<div class="detail-item"><span class="detail-label">Cron</span><code class="detail-code">{{ task.cron_expression }}</code></div>
<div class="detail-item"><span class="detail-label">容忍窗口</span><span class="detail-value">{{ task.grace_period }}s</span></div>
<div class="detail-item"><span class="detail-label">最近运行</span><span class="detail-value mono">{{ task.last_run_at ? dayjs(task.last_run_at).format('YYYY-MM-DD HH:mm:ss') : '—' }}</span></div>
<div class="detail-item"><span class="detail-label">最近运行</span><span class="detail-value mono">{{ task.last_run_at ? beijing(task.last_run_at, 'YYYY-MM-DD HH:mm:ss') : '—' }}</span></div>
<div class="detail-item">
<span class="detail-label">最近结果</span>
<span v-if="task.last_run_result" class="tag" :class="task.last_run_result === 'success' ? 'tag-green' : 'tag-red'">
@@ -25,7 +25,7 @@
<span v-else class="detail-value"></span>
</div>
<div class="detail-item"><span class="detail-label">执行次数</span><span class="detail-value mono">{{ task.total_run_count }}</span></div>
<div class="detail-item"><span class="detail-label">预计下次</span><span class="detail-value mono">{{ task.next_run_at ? dayjs(task.next_run_at).format('YYYY-MM-DD HH:mm:ss') : '待计算' }}</span></div>
<div class="detail-item"><span class="detail-label">预计下次</span><span class="detail-value mono">{{ task.next_run_at ? beijing(task.next_run_at, 'YYYY-MM-DD HH:mm:ss') : '待计算' }}</span></div>
<div class="detail-item"><span class="detail-label">描述</span><span class="detail-value">{{ task.description || '无' }}</span></div>
</div>
</div>
@@ -55,7 +55,7 @@
{{ e.status === 'success' ? '成功' : e.status === 'failed' ? '失败' : '运行中' }}
</span>
</td>
<td class="cell-mono">{{ dayjs(e.started_at).format('MM-DD HH:mm:ss') }}</td>
<td class="cell-mono">{{ beijing(e.started_at, 'MM-DD HH:mm:ss') }}</td>
<td class="cell-mono">{{ e.duration_ms ?? '—' }}</td>
<td class="cell-muted cell-ellipsis">{{ e.result || '—' }}</td>
<td><button class="cell-link-btn" @click="showLog(e)">日志</button></td>
@@ -86,7 +86,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import dayjs from 'dayjs'
import { beijing } from '../utils/time.js'
import { getTask, listExecutions } from '../api/index.js'
const route = useRoute()
+131 -13
View File
@@ -1,11 +1,18 @@
<template>
<div class="tasks-page">
<div class="section mb-24">
<div class="section-header">
<div class="section-header collapsible" @click="showGuide = !showGuide">
<h3>定时任务 AI Agent 自动管理</h3>
<div class="section-toggle">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
:style="{ transform: showGuide ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }">
<polyline points="6 9 12 15 18 9"/>
</svg>
<span class="toggle-label">{{ showGuide ? '收起指令' : '展开指令' }}</span>
</div>
</div>
<div class="onboard-body">
<p>Agent 注册后通过下方 API 汇报任务执行情况无需手动创建</p>
<div v-if="showGuide" class="onboard-body">
<p>将下方指令发送给您的 AI Agent它会自动汇报执行结果并管理定时任务</p>
<div class="code-block">
<div class="code-tabs">
<button :class="{ active: tab === 'report' }" @click="tab = 'report'">汇报执行</button>
@@ -51,7 +58,7 @@
{{ t.status === 'active' ? '运行中' : t.status === 'paused' ? '已暂停' : '已停止' }}
</span>
</td>
<td class="cell-mono">{{ t.last_run_at ? dayjs(t.last_run_at).format('MM-DD HH:mm') : '—' }}</td>
<td class="cell-mono">{{ t.last_run_at ? beijing(t.last_run_at) : '—' }}</td>
<td>
<span v-if="t.last_run_result" class="tag" :class="t.last_run_result === 'success' ? 'tag-green' : 'tag-red'">
{{ t.last_run_result === 'success' ? '成功' : '失败' }}
@@ -78,11 +85,12 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import dayjs from 'dayjs'
import { beijing } from '../utils/time.js'
import { listTasks, deleteTask, getSystemConfig } from '../api/index.js'
const tasks = ref([])
const tab = ref('report')
const showGuide = ref(false)
const baseUrl = ref(window.location.origin)
const loadData = async () => {
@@ -106,17 +114,120 @@ const handleDelete = async (row) => {
const codes = computed(() => {
const b = baseUrl.value
return {
report: `curl -X POST ${b}/api/tasks/<TASK_ID>/executions \\
report: `# TaskPulse — 汇报执行结果
你已注册到 TaskPulse。每次任务运行后(无论成功失败),主动汇报执行结果。
## 汇报成功
curl -X POST ${b}/api/tasks/<TASK_ID>/executions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <YOUR_API_KEY>" \\
-d '{"status":"success","duration_ms":2500,"log":"done"}'`,
register: `curl -X POST "${b}/api/tasks?agent_id=<AGENT_ID>" \\
-H "Authorization: Bearer <API_KEY>" \\
-d '{"status":"success","duration_ms":2500,"log":"[INFO] 任务执行完成,同步了100条记录"}'
## 汇报失败
curl -X POST ${b}/api/tasks/<TASK_ID>/executions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <YOUR_API_KEY>" \\
-d '{"name":"data-sync","cron_expression":"*/5 * * * *","grace_period":300}'`,
batch: `curl -X POST ${b}/api/agents/register-with-tasks \\
-H "Authorization: Bearer <API_KEY>" \\
-d '{"status":"failed","duration_ms":1200,"log":"[ERROR] 连接超时","error_message":"TimeoutError: connection to DB timed out"}'
## 修改你的代码自动汇报
在你的每个定时任务函数末尾,加上汇报逻辑。参考以下实现:
import time
import requests
def run_task(task_func, task_id, api_key):
"""包裹你的任务函数,自动汇报执行结果"""
start = time.time()
try:
result = task_func()
elapsed = int((time.time() - start) * 1000)
requests.post(f"${b}/api/tasks/{task_id}/executions",
headers={"Authorization": f"Bearer {api_key}"},
json={"status": "success", "duration_ms": elapsed, "log": str(result)})
except Exception as e:
elapsed = int((time.time() - start) * 1000)
requests.post(f"${b}/api/tasks/{task_id}/executions",
headers={"Authorization": f"Bearer {api_key}"},
json={"status": "failed", "duration_ms": elapsed, "error_message": str(e)})
# 在你的循环/调度中这样使用:
run_task(my_task_func, TASK_ID, API_KEY)`,
register: `# TaskPulse — 注册新任务
你需要注册自己管理的每一个定时任务,系统才知道你的执行计划。
## 单个注册
curl -X POST "${b}/api/tasks?agent_id=<AGENT_ID>" \\
-H "Content-Type: application/json" \\
-d '{"name":"agent","tasks":[{"name":"sync","cron_expression":"*/5 * * * *"}]}'`,
-H "Authorization: Bearer <API_KEY>" \\
-d '{"name":"数据同步","cron_expression":"*/5 * * * *","description":"每5分钟同步一次数据","grace_period":300}'
参数说明:
- name: 任务名称(必填)
- cron_expression: Cron 表达式,格式:分 时 日 月 周(必填)
- description: 任务描述(可选)
- grace_period: 容忍窗口(秒),任务超时N秒未执行则告警(默认300)
## 查看已注册的任务
curl -X GET "${b}/api/tasks" \\
-H "Authorization: Bearer <API_KEY>"
## 更新现有任务(修改 cron 等)
curl -X PUT "${b}/api/tasks/<TASK_ID>" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <API_KEY>" \\
-d '{"cron_expression":"0 * * * *","status":"active"}'
## 删除任务
curl -X DELETE "${b}/api/tasks/<TASK_ID>" \\
-H "Authorization: Bearer <API_KEY>"`,
batch: `# TaskPulse — 批量注册(推荐)
如果你有多个定时任务,推荐使用批量注册接口,一次完成 Agent 注册 + 所有任务登记。
## 批量注册 Agent + 全部任务
curl -X POST ${b}/api/agents/register-with-tasks \\
-H "Content-Type: application/json" \\
-d '{
"name": "<你的Agent名称>",
"description": "<你的职责描述>",
"tasks": [
{"name": "数据同步", "cron_expression": "*/5 * * * *", "description": "定时同步数据", "grace_period": 300},
{"name": "日报通知", "cron_expression": "0 9 * * *", "description": "每天早上9点生成报告", "grace_period": 600},
{"name": "周报汇总", "cron_expression": "0 10 * * 1", "description": "每周一早10点汇总", "grace_period": 900}
]
}'
响应中包含 agent 信息和任务创建数量。返回的 api_key 请保存好。
## Python 示例
import requests
resp = requests.post(f"${b}/api/agents/register-with-tasks", json={
"name": "data-agent",
"description": "数据相关的全部定时任务",
"tasks": [
{"name": "sync-data", "cron_expression": "*/5 * * * *"},
{"name": "daily-report", "cron_expression": "0 9 * * *"}
]
})
result = resp.json()
print(f"Agent ID: {result['agent']['id']}, 任务数: {result['tasks_created']}")
API_KEY = result["agent"]["api_key"]
---
注册完成后,回到仪表盘页面,将「汇报执行结果」的指令一并发送给 AI Agent,它就知道如何自动向你汇报运行状态。`,
}
})
@@ -146,6 +257,13 @@ onMounted(loadData)
border-bottom: 1px solid var(--border-color);
}
.section-header h3 { font-size: 15px; font-weight: 600; color: #e0e0f0; margin: 0; }
.section-header.collapsible { cursor: pointer; user-select: none; }
.section-header.collapsible:hover { background: rgba(255,255,255,0.02); }
.section-toggle {
display: flex; align-items: center; gap: 6px;
color: var(--text-muted); font-size: 12px;
}
.section-toggle:hover { color: var(--text-secondary); }
.onboard-body { padding: 20px; }
.onboard-body > p { font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; }