diff --git a/CHANGELOG.md b/CHANGELOG.md index 7159128c..78c6a5c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ## [Unreleased] +### 新增 +- 定时任务支持查看执行记录:在操作列或卡片更多菜单打开弹窗,按服务端分页展示完成时间、状态和失败原因。 + ## [0.9.22] - 2026-08-11 ### 新增 diff --git a/dashboard/src/api/modules/cronjob.ts b/dashboard/src/api/modules/cronjob.ts index 24371543..34c819b8 100644 --- a/dashboard/src/api/modules/cronjob.ts +++ b/dashboard/src/api/modules/cronjob.ts @@ -3,6 +3,7 @@ import type { OctopCronRow, OctopCronCreateBody, OctopCronPatchBody, + OctopCronRunPage, } from "../types"; export interface OctopCronSettings { @@ -36,4 +37,9 @@ export const octopCronApi = { request(`/agents/${agentId}/cron/${cronId}/run-now`, { method: "POST", }), + + listRuns: (agentId: string, cronId: string, page: number, pageSize: number) => + request( + `/agents/${agentId}/cron/${cronId}/runs?page=${page}&page_size=${pageSize}`, + ), }; diff --git a/dashboard/src/api/types/cronjob.ts b/dashboard/src/api/types/cronjob.ts index e08f4f96..be5ce89e 100644 --- a/dashboard/src/api/types/cronjob.ts +++ b/dashboard/src/api/types/cronjob.ts @@ -103,3 +103,17 @@ export interface OctopCronPatchBody { task_type?: "text" | "agent"; mcp_servers?: string[]; } + +export interface OctopCronRun { + id: number; + completed_at: number; + status: "ok" | "error"; + error: string | null; +} + +export interface OctopCronRunPage { + items: OctopCronRun[]; + page: number; + page_size: number; + total: number; +} diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index d7b0f2d6..ac458fbf 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -1152,6 +1152,18 @@ "operationFailed": "Operation failed", "triggeredSuccess": "Task triggered, result will appear shortly", "executeFailed": "Failed to trigger execution", + "runHistory": { + "action": "Execution Records", + "title": "Execution Records", + "titleWithName": "Execution Records · {{name}}", + "completedAt": "Completed At", + "status": "Status", + "error": "Failure Reason", + "succeeded": "Succeeded", + "failed": "Failed", + "empty": "No execution records", + "loadFailed": "Failed to load execution records" + }, "totalItems": "Total {{count}} items", "loadingJobs": "Loading this expert's tasks…", "syncing": "Syncing…", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 6b411fe3..94d9903c 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -1149,6 +1149,18 @@ "operationFailed": "操作失败", "triggeredSuccess": "任务已触发,结果稍后显示", "executeFailed": "触发执行失败", + "runHistory": { + "action": "执行记录", + "title": "执行记录", + "titleWithName": "执行记录 · {{name}}", + "completedAt": "完成时间", + "status": "状态", + "error": "失败原因", + "succeeded": "成功", + "failed": "失败", + "empty": "暂无执行记录", + "loadFailed": "加载执行记录失败" + }, "totalItems": "共 {{count}} 项", "loadingJobs": "正在加载当前专家的任务…", "syncing": "正在同步…", diff --git a/dashboard/src/pages/Control/CronJobs/components/CronJobCard.tsx b/dashboard/src/pages/Control/CronJobs/components/CronJobCard.tsx index 81ff116d..ecf28431 100644 --- a/dashboard/src/pages/Control/CronJobs/components/CronJobCard.tsx +++ b/dashboard/src/pages/Control/CronJobs/components/CronJobCard.tsx @@ -33,6 +33,7 @@ interface CronJobCardProps { timeZone: string; onToggleEnabled: (job: CronJob) => void; onExecuteNow: (job: CronJob) => void; + onViewRuns: (job: CronJob) => void; onEdit: (job: CronJob) => void; onDelete: (jobId: string) => void; } @@ -42,6 +43,7 @@ export function CronJobCard({ timeZone, onToggleEnabled, onExecuteNow, + onViewRuns, onEdit, onDelete, }: CronJobCardProps) { @@ -73,6 +75,11 @@ export function CronJobCard({ const taskType = job.task_type === "text" ? "text" : "agent"; const moreMenuItems: MenuProps["items"] = [ + { + key: "runs", + label: t("cronJobs.runHistory.action"), + onClick: () => onViewRuns(job), + }, { key: "edit", label: t("common.edit"), diff --git a/dashboard/src/pages/Control/CronJobs/components/RunHistoryModal.tsx b/dashboard/src/pages/Control/CronJobs/components/RunHistoryModal.tsx new file mode 100644 index 00000000..2eeb1f7a --- /dev/null +++ b/dashboard/src/pages/Control/CronJobs/components/RunHistoryModal.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { Empty, Modal, Table, Tag, Tooltip, message } from "antd"; +import type { TableColumnsType } from "antd"; +import { useTranslation } from "react-i18next"; +import { octopCronApi } from "../../../../api/modules/cronjob"; +import type { OctopCronRun } from "../../../../api/types"; +import { formatCronTimestamp } from "../cronDisplay"; + +const DEFAULT_PAGE_SIZE = 10; + +interface RunHistoryModalProps { + open: boolean; + agentId: string; + cronId: string | null; + jobName?: string; + timeZone: string; + onClose: () => void; +} + +export function RunHistoryModal({ + open, + agentId, + cronId, + jobName, + timeZone, + onClose, +}: RunHistoryModalProps) { + const { t } = useTranslation(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [total, setTotal] = useState(0); + + useEffect(() => { + if (!open || !cronId) return; + let cancelled = false; + setLoading(true); + void octopCronApi + .listRuns(agentId, cronId, page, pageSize) + .then((result) => { + if (cancelled) return; + setItems(result.items); + setTotal(result.total); + }) + .catch((error) => { + if (cancelled) return; + console.error("Failed to load cron run history", error); + message.error(t("cronJobs.runHistory.loadFailed")); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [agentId, cronId, open, page, pageSize, t]); + + useEffect(() => { + if (!open) { + setItems([]); + setPage(1); + setPageSize(DEFAULT_PAGE_SIZE); + setTotal(0); + } + }, [open]); + + const columns: TableColumnsType = [ + { + title: t("cronJobs.runHistory.completedAt"), + dataIndex: "completed_at", + width: 190, + render: (value: number) => formatCronTimestamp(value, timeZone), + }, + { + title: t("cronJobs.runHistory.status"), + dataIndex: "status", + width: 100, + render: (status: OctopCronRun["status"]) => ( + + {status === "ok" + ? t("cronJobs.runHistory.succeeded") + : t("cronJobs.runHistory.failed")} + + ), + }, + { + title: t("cronJobs.runHistory.error"), + dataIndex: "error", + ellipsis: true, + render: (error: string | null) => + error ? ( + + {error} + + ) : ( + "—" + ), + }, + ]; + + return ( + + + rowKey="id" + columns={columns} + dataSource={items} + loading={loading} + locale={{ + emptyText: , + }} + scroll={{ x: 620, y: 420 }} + pagination={{ + current: page, + pageSize, + total, + showSizeChanger: true, + pageSizeOptions: [10, 20, 50], + showTotal: (count) => t("cronJobs.totalItems", { count }), + onChange: (nextPage, nextPageSize) => { + setPage(nextPageSize === pageSize ? nextPage : 1); + setPageSize(nextPageSize); + }, + }} + /> + + ); +} diff --git a/dashboard/src/pages/Control/CronJobs/components/columns.tsx b/dashboard/src/pages/Control/CronJobs/components/columns.tsx index dcfbfb40..16b73c79 100644 --- a/dashboard/src/pages/Control/CronJobs/components/columns.tsx +++ b/dashboard/src/pages/Control/CronJobs/components/columns.tsx @@ -20,6 +20,7 @@ interface ColumnHandlers { onDetail: (job: CronJob) => void; onToggleEnabled: (job: CronJob) => void; onExecuteNow: (job: CronJob) => void; + onViewRuns: (job: CronJob) => void; onEdit: (job: CronJob) => void; onDelete: (jobId: string) => void; t: TFunction; @@ -248,7 +249,7 @@ export const createColumns = ( { title: handlers.t("cronJobs.action"), key: "action", - width: 200, + width: 280, fixed: "right", render: (_: unknown, record: CronJob) => { const menuItems: MenuProps["items"] = [ @@ -285,6 +286,13 @@ export const createColumns = ( > {handlers.t("cronJobs.executeNow")} +