v0.1.4-p4

main
mula.liu 2026-03-31 14:04:34 +08:00
parent 41212a7ac9
commit 1d20c8edb4
17 changed files with 265 additions and 61 deletions

View File

@ -1,7 +1,9 @@
import logging
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
from sqlmodel import Session
@ -21,6 +23,40 @@ router = APIRouter()
logger = logging.getLogger("dashboard.backend")
def _now_ms() -> int:
return int(time.time() * 1000)
def _compute_cron_next_run(schedule: Dict[str, Any], now_ms: Optional[int] = None) -> Optional[int]:
current_ms = int(now_ms or _now_ms())
kind = str(schedule.get("kind") or "").strip().lower()
if kind == "at":
at_ms = int(schedule.get("atMs") or 0)
return at_ms if at_ms > current_ms else None
if kind == "every":
every_ms = int(schedule.get("everyMs") or 0)
return current_ms + every_ms if every_ms > 0 else None
if kind == "cron":
expr = str(schedule.get("expr") or "").strip()
if not expr:
return None
try:
from croniter import croniter
tz_name = str(schedule.get("tz") or "").strip()
tz = ZoneInfo(tz_name) if tz_name else datetime.now().astimezone().tzinfo
base_dt = datetime.fromtimestamp(current_ms / 1000, tz=tz)
next_dt = croniter(expr, base_dt).get_next(datetime)
return int(next_dt.timestamp() * 1000)
except Exception:
return None
return None
def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance:
bot = session.get(BotInstance, bot_id)
if not bot:
@ -129,11 +165,43 @@ def stop_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_sessi
if not found:
raise HTTPException(status_code=404, detail="Cron job not found")
found["enabled"] = False
found["updatedAtMs"] = int(datetime.utcnow().timestamp() * 1000)
found["updatedAtMs"] = _now_ms()
state = found.get("state")
if not isinstance(state, dict):
state = {}
found["state"] = state
state["nextRunAtMs"] = None
_write_cron_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": jobs})
return {"status": "stopped", "job_id": job_id}
@router.post("/api/bots/{bot_id}/cron/jobs/{job_id}/start")
def start_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_session)):
_get_bot_or_404(session, bot_id)
store = _read_cron_store(bot_id)
jobs = store.get("jobs", [])
if not isinstance(jobs, list):
jobs = []
found = None
for row in jobs:
if isinstance(row, dict) and str(row.get("id")) == job_id:
found = row
break
if not found:
raise HTTPException(status_code=404, detail="Cron job not found")
found["enabled"] = True
found["updatedAtMs"] = _now_ms()
state = found.get("state")
if not isinstance(state, dict):
state = {}
found["state"] = state
schedule = found.get("schedule")
state["nextRunAtMs"] = _compute_cron_next_run(schedule if isinstance(schedule, dict) else {})
_write_cron_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": jobs})
return {"status": "started", "job_id": job_id}
@router.delete("/api/bots/{bot_id}/cron/jobs/{job_id}")
def delete_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_session)):
_get_bot_or_404(session, bot_id)

View File

@ -282,7 +282,7 @@ def _normalize_initial_channels(bot_id: str, channels: Optional[List[ChannelConf
return rows
def _sync_workspace_channels(
def _sync_workspace_channels_impl(
session: Session,
bot_id: str,
snapshot: Dict[str, Any],

View File

@ -18,8 +18,8 @@ from services.bot_channel_service import (
_get_bot_channels_from_config,
_normalize_channel_extra,
_read_global_delivery_flags,
_sync_workspace_channels,
)
from services.bot_service import _sync_workspace_channels
from services.bot_mcp_service import (
_merge_mcp_servers_preserving_extras,
_normalize_mcp_servers,

View File

@ -218,7 +218,9 @@ def list_bots_with_cache(session: Session) -> List[Dict[str, Any]]:
cached = cache.get_json(_cache_key_bots_list())
if isinstance(cached, list):
return cached
bots = session.exec(select(BotInstance)).all()
bots = session.exec(
select(BotInstance).order_by(BotInstance.created_at.desc(), BotInstance.id.asc())
).all()
dirty = False
for bot in bots:
actual_status = docker_manager.get_bot_status(bot.id)

View File

@ -32,7 +32,7 @@ from services.bot_channel_service import (
_normalize_channel_extra,
_normalize_initial_channels,
_read_global_delivery_flags,
_sync_workspace_channels as _sync_workspace_channels_impl,
_sync_workspace_channels_impl,
)
from services.bot_mcp_service import (
_merge_mcp_servers_preserving_extras,

View File

@ -95,6 +95,16 @@ def _read_json_object(path: str) -> Dict[str, Any]:
return {}
def _read_json_value(path: str) -> Any:
if not os.path.isfile(path):
return None
try:
with open(path, "r", encoding="utf-8") as file:
return json.load(file)
except Exception:
return None
def _write_json_atomic(path: str, payload: Dict[str, Any]) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp_path = f"{path}.tmp"
@ -196,23 +206,32 @@ def _write_env_store(bot_id: str, env_params: Dict[str, str]) -> None:
def _cron_store_path(bot_id: str) -> str:
return os.path.join(_bot_data_root(bot_id), "cron", "jobs.json")
return os.path.join(_workspace_root(bot_id), "cron", "jobs.json")
def _normalize_cron_store_payload(raw: Any) -> Dict[str, Any]:
if isinstance(raw, list):
return {"version": 1, "jobs": [row for row in raw if isinstance(row, dict)]}
if not isinstance(raw, dict):
return {"version": 1, "jobs": []}
jobs = raw.get("jobs")
if isinstance(jobs, list):
normalized_jobs = [row for row in jobs if isinstance(row, dict)]
else:
normalized_jobs = []
return {
"version": _safe_int(raw.get("version"), 1),
"jobs": normalized_jobs,
}
def _read_cron_store(bot_id: str) -> Dict[str, Any]:
data = _read_json_object(_cron_store_path(bot_id))
if not data:
return {"version": 1, "jobs": []}
jobs = data.get("jobs")
if not isinstance(jobs, list):
data["jobs"] = []
if "version" not in data:
data["version"] = 1
return data
return _normalize_cron_store_payload(_read_json_value(_cron_store_path(bot_id)))
def _write_cron_store(bot_id: str, store: Dict[str, Any]) -> None:
_write_json_atomic(_cron_store_path(bot_id), store)
normalized = _normalize_cron_store_payload(store)
_write_json_atomic(_cron_store_path(bot_id), normalized)
def _sessions_root(bot_id: str) -> str:

View File

@ -281,8 +281,10 @@ export const dashboardEn = {
cronEmpty: 'No scheduled jobs.',
cronEnabled: 'Enabled',
cronDisabled: 'Disabled',
cronStart: 'Enable job',
cronStop: 'Stop job',
cronDelete: 'Delete job',
cronStartFail: 'Failed to enable job.',
cronStopFail: 'Failed to stop job.',
cronDeleteFail: 'Failed to delete job.',
cronDeleteConfirm: (id: string) => `Delete scheduled job ${id}?`,

View File

@ -281,8 +281,10 @@ export const dashboardZhCn = {
cronEmpty: '暂无定时任务。',
cronEnabled: '启用',
cronDisabled: '已停用',
cronStart: '启用任务',
cronStop: '停止任务',
cronDelete: '删除任务',
cronStartFail: '启用任务失败。',
cronStopFail: '停止任务失败。',
cronDeleteFail: '删除任务失败。',
cronDeleteConfirm: (id: string) => `确认删除任务 ${id}`,

View File

@ -220,6 +220,7 @@ export function BotListPanel({
const isEnabling = controlState === 'enabling';
const isDisabling = controlState === 'disabling';
const isRunning = String(bot.docker_status || '').toUpperCase() === 'RUNNING';
const showActionPending = isOperating && !isEnabling && !isDisabling;
return (
<div
key={bot.id}
@ -290,7 +291,7 @@ export function BotListPanel({
tooltip={isRunning ? labels.stop : labels.start}
aria-label={isRunning ? labels.stop : labels.start}
>
{isStarting || isStopping ? (
{showActionPending ? (
<span className="ops-control-pending">
<span className="ops-control-dots" aria-hidden="true">
<i />

View File

@ -111,3 +111,69 @@
align-items: center;
gap: 6px;
}
.ops-cron-action-stop {
background: color-mix(in oklab, #f5af48 30%, var(--panel-soft) 70%);
border-color: color-mix(in oklab, #f5af48 58%, var(--line) 42%);
color: #5e3b00;
}
.ops-cron-action-start {
background: color-mix(in oklab, var(--ok) 24%, var(--panel-soft) 76%);
border-color: color-mix(in oklab, var(--ok) 52%, var(--line) 48%);
color: color-mix(in oklab, var(--text) 76%, white 24%);
}
.ops-cron-action-start:hover {
background: color-mix(in oklab, var(--ok) 32%, var(--panel-soft) 68%);
border-color: color-mix(in oklab, var(--ok) 64%, var(--line) 36%);
}
.ops-cron-action-stop:hover {
background: color-mix(in oklab, #f5af48 38%, var(--panel-soft) 62%);
border-color: color-mix(in oklab, #f5af48 70%, var(--line) 30%);
}
.ops-cron-control-pending {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
}
.ops-cron-control-dots {
display: inline-flex;
align-items: center;
gap: 2px;
}
.ops-cron-control-dots i {
width: 3px;
height: 3px;
border-radius: 999px;
background: currentColor;
opacity: 0.38;
animation: ops-cron-dot-pulse 1s infinite ease-in-out;
}
.ops-cron-control-dots i:nth-child(2) {
animation-delay: 0.12s;
}
.ops-cron-control-dots i:nth-child(3) {
animation-delay: 0.24s;
}
@keyframes ops-cron-dot-pulse {
0%,
80%,
100% {
opacity: 0.24;
transform: translateY(0);
}
40% {
opacity: 1;
transform: translateY(-1px);
}
}

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Clock3, Plus, PowerOff, RefreshCw, Save, Trash2, X } from 'lucide-react';
import { Clock3, Plus, Power, RefreshCw, Save, Square, Trash2, X } from 'lucide-react';
import { DrawerShell } from '../../../components/DrawerShell';
import { PasswordInput } from '../../../components/PasswordInput';
@ -37,6 +37,7 @@ interface CronModalLabels {
cronEnabled: string;
cronLoading: string;
cronReload: string;
cronStart: string;
cronStop: string;
cronViewer: string;
}
@ -299,11 +300,13 @@ interface CronJobsModalProps {
cronLoading: boolean;
cronJobs: CronJob[];
cronActionJobId: string;
cronActionType?: 'starting' | 'stopping' | 'deleting' | '';
isZh: boolean;
labels: CronModalLabels;
formatCronSchedule: (job: CronJob, isZh: boolean) => string;
onClose: () => void;
onReload: () => Promise<void> | void;
onStartJob: (jobId: string) => Promise<void> | void;
onStopJob: (jobId: string) => Promise<void> | void;
onDeleteJob: (jobId: string) => Promise<void> | void;
}
@ -313,11 +316,13 @@ export function CronJobsModal({
cronLoading,
cronJobs,
cronActionJobId,
cronActionType,
isZh,
labels,
formatCronSchedule,
onClose,
onReload,
onStartJob,
onStopJob,
onDeleteJob,
}: CronJobsModalProps) {
@ -352,7 +357,8 @@ export function CronJobsModal({
) : (
<div className="ops-cron-list ops-cron-list-scroll">
{cronJobs.map((job) => {
const stopping = cronActionJobId === job.id;
const acting = cronActionJobId === job.id && Boolean(cronActionType);
const enabled = job.enabled !== false;
const channel = String(job.payload?.channel || '').trim();
const to = String(job.payload?.to || '').trim();
const target = channel && to ? `${channel}:${to}` : channel || to || '-';
@ -373,20 +379,28 @@ export function CronJobsModal({
</div>
<div className="ops-cron-actions">
<LucentIconButton
className="btn btn-danger btn-sm icon-btn"
onClick={() => void onStopJob(job.id)}
tooltip={labels.cronStop}
aria-label={labels.cronStop}
disabled={stopping || job.enabled === false}
className={`btn btn-sm icon-btn ${enabled ? 'ops-cron-action-stop' : 'ops-cron-action-start'}`}
onClick={() => void (enabled ? onStopJob(job.id) : onStartJob(job.id))}
tooltip={enabled ? labels.cronStop : labels.cronStart}
aria-label={enabled ? labels.cronStop : labels.cronStart}
disabled={acting}
>
<PowerOff size={13} />
{acting ? (
<span className="ops-cron-control-pending">
<span className="ops-cron-control-dots" aria-hidden="true">
<i />
<i />
<i />
</span>
</span>
) : enabled ? <Square size={13} /> : <Power size={13} />}
</LucentIconButton>
<LucentIconButton
className="btn btn-danger btn-sm icon-btn"
onClick={() => void onDeleteJob(job.id)}
tooltip={labels.cronDelete}
aria-label={labels.cronDelete}
disabled={stopping}
disabled={acting}
>
<Trash2 size={13} />
</LucentIconButton>

View File

@ -195,6 +195,7 @@ export function useBotDashboardModule({
const {
botSkills,
cronActionJobId,
cronActionType,
cronJobs,
cronLoading,
createEnvParam,
@ -218,6 +219,7 @@ export function useBotDashboardModule({
removeBotSkill,
resetSupportState,
saveSingleEnvParam,
startCronJob,
setTopicFeedTopicKey,
stopCronJob,
topicFeedDeleteSavingById,
@ -261,6 +263,7 @@ export function useBotDashboardModule({
closeRuntimeMenu: () => setRuntimeMenuOpen(false),
confirm,
cronActionJobId,
cronActionType,
cronJobs,
cronLoading,
createEnvParam,
@ -291,6 +294,7 @@ export function useBotDashboardModule({
saveSingleEnvParam,
selectedBot,
selectedBotId,
startCronJob,
stopCronJob,
t,
weixinLoginStatus,

View File

@ -29,6 +29,7 @@ interface UseDashboardConfigPanelsOptions {
botSkills: WorkspaceSkillOption[];
confirm: (options: ConfirmOptions) => Promise<boolean>;
cronActionJobId: string | null;
cronActionType?: 'starting' | 'stopping' | 'deleting' | '';
cronJobs: any[];
cronLoading: boolean;
createEnvParam: (key: string, value: string) => Promise<boolean>;
@ -58,6 +59,7 @@ interface UseDashboardConfigPanelsOptions {
saveSingleEnvParam: (originalKey: string, nextKey: string, nextValue: string) => Promise<boolean>;
selectedBot?: any;
selectedBotId: string;
startCronJob: (jobId: string) => Promise<void>;
stopCronJob: (jobId: string) => Promise<void>;
t: any;
lc: any;
@ -69,6 +71,7 @@ export function useDashboardConfigPanels({
botSkills,
confirm,
cronActionJobId,
cronActionType,
cronJobs,
cronLoading,
createEnvParam,
@ -98,6 +101,7 @@ export function useDashboardConfigPanels({
saveSingleEnvParam,
selectedBot,
selectedBotId,
startCronJob,
stopCronJob,
t,
lc,
@ -450,6 +454,7 @@ export function useDashboardConfigPanels({
const cronJobsModalProps = buildCronJobsModalProps({
cronActionJobId,
cronActionType,
cronJobs,
cronLoading,
deleteCronJob,
@ -462,12 +467,14 @@ export function useDashboardConfigPanels({
cronEnabled: t.cronEnabled,
cronLoading: t.cronLoading,
cronReload: t.cronReload,
cronStart: t.cronStart,
cronStop: t.cronStop,
cronViewer: t.cronViewer,
},
loadCronJobs,
onClose: () => setShowCronModal(false),
selectedBot,
startCronJob,
stopCronJob,
open: showCronModal,
});

View File

@ -51,6 +51,7 @@ export function useDashboardRuntimeControl({
notify,
confirm,
}: UseDashboardRuntimeControlOptions) {
const CONTROL_MIN_VISIBLE_MS = 450;
const [showResourceModal, setShowResourceModal] = useState(false);
const [resourceBotId, setResourceBotId] = useState('');
const [resourceSnapshot, setResourceSnapshot] = useState<BotResourceSnapshot | null>(null);
@ -239,16 +240,27 @@ export function useDashboardRuntimeControl({
}
}, [bots, confirm, isBatchOperating, notify, refresh, t, updateBotStatus]);
const ensureControlVisible = useCallback(async (startedAt: number) => {
const elapsed = Date.now() - startedAt;
const remain = CONTROL_MIN_VISIBLE_MS - elapsed;
if (remain > 0) {
await new Promise((resolve) => window.setTimeout(resolve, remain));
}
}, []);
const stopBot = useCallback(async (id: string, status: string) => {
if (status !== 'RUNNING') return;
const startedAt = Date.now();
setOperatingBotId(id);
setControlStateByBot((prev) => ({ ...prev, [id]: 'stopping' }));
try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${id}/stop`);
updateBotStatus(id, 'STOPPED');
await refresh();
await ensureControlVisible(startedAt);
} catch {
notify(t.stopFail, { tone: 'error' });
await ensureControlVisible(startedAt);
} finally {
setOperatingBotId(null);
setControlStateByBot((prev) => {
@ -257,19 +269,22 @@ export function useDashboardRuntimeControl({
return next;
});
}
}, [notify, refresh, t.stopFail, updateBotStatus]);
}, [ensureControlVisible, notify, refresh, t.stopFail, updateBotStatus]);
const startBot = useCallback(async (id: string, status: string) => {
if (status === 'RUNNING') return;
const startedAt = Date.now();
setOperatingBotId(id);
setControlStateByBot((prev) => ({ ...prev, [id]: 'starting' }));
try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${id}/start`);
updateBotStatus(id, 'RUNNING');
await refresh();
await ensureControlVisible(startedAt);
} catch (error: any) {
await refresh();
notify(error?.response?.data?.detail || t.startFail, { tone: 'error' });
await ensureControlVisible(startedAt);
} finally {
setOperatingBotId(null);
setControlStateByBot((prev) => {
@ -278,7 +293,7 @@ export function useDashboardRuntimeControl({
return next;
});
}
}, [notify, refresh, t.startFail, updateBotStatus]);
}, [ensureControlVisible, notify, refresh, t.startFail, updateBotStatus]);
const restartBot = useCallback(async (id: string, status: string) => {
const normalized = String(status || '').toUpperCase();
@ -288,6 +303,7 @@ export function useDashboardRuntimeControl({
tone: 'warning',
});
if (!ok) return;
const startedAt = Date.now();
setOperatingBotId(id);
try {
if (normalized === 'RUNNING') {
@ -299,9 +315,11 @@ export function useDashboardRuntimeControl({
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${id}/start`);
updateBotStatus(id, 'RUNNING');
await refresh();
await ensureControlVisible(startedAt);
} catch (error: any) {
await refresh();
notify(error?.response?.data?.detail || t.restartFail, { tone: 'error' });
await ensureControlVisible(startedAt);
} finally {
setOperatingBotId(null);
setControlStateByBot((prev) => {
@ -310,7 +328,7 @@ export function useDashboardRuntimeControl({
return next;
});
}
}, [confirm, notify, refresh, t, updateBotStatus]);
}, [confirm, ensureControlVisible, notify, refresh, t, updateBotStatus]);
const setBotEnabled = useCallback(async (id: string, enabled: boolean) => {
setOperatingBotId(id);

View File

@ -36,40 +36,14 @@ export function useDashboardShellState({
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
const botListMenuRef = useRef<HTMLDivElement | null>(null);
const controlCommandPanelRef = useRef<HTMLDivElement | null>(null);
const botOrderRef = useRef<Record<string, number>>({});
const nextBotOrderRef = useRef(1);
useEffect(() => {
const ordered = Object.values(activeBots).sort((a, b) => {
const aCreated = parseBotTimestamp(a.created_at);
const bCreated = parseBotTimestamp(b.created_at);
if (aCreated !== bCreated) return aCreated - bCreated;
return String(a.id || '').localeCompare(String(b.id || ''));
});
ordered.forEach((bot) => {
const id = String(bot.id || '').trim();
if (!id) return;
if (botOrderRef.current[id] !== undefined) return;
botOrderRef.current[id] = nextBotOrderRef.current;
nextBotOrderRef.current += 1;
});
const alive = new Set(ordered.map((bot) => String(bot.id || '').trim()).filter(Boolean));
Object.keys(botOrderRef.current).forEach((id) => {
if (!alive.has(id)) delete botOrderRef.current[id];
});
}, [activeBots]);
const bots = useMemo(
() =>
Object.values(activeBots).sort((a, b) => {
const aId = String(a.id || '').trim();
const bId = String(b.id || '').trim();
const aOrder = botOrderRef.current[aId] ?? Number.MAX_SAFE_INTEGER;
const bOrder = botOrderRef.current[bId] ?? Number.MAX_SAFE_INTEGER;
if (aOrder !== bOrder) return aOrder - bOrder;
return aId.localeCompare(bId);
const aCreated = parseBotTimestamp(a.created_at);
const bCreated = parseBotTimestamp(b.created_at);
if (aCreated !== bCreated) return bCreated - aCreated;
return String(a.id || '').localeCompare(String(b.id || ''));
}),
[activeBots],
);
@ -166,7 +140,6 @@ export function useDashboardShellState({
botListPage,
botListTotalPages,
botListQuery,
botOrderRef,
bots,
compactListFirstMode,
compactPanelTab,
@ -177,7 +150,6 @@ export function useDashboardShellState({
hasForcedBot,
isCompactListPage,
isCompactMobile,
nextBotOrderRef,
normalizedBotListQuery,
pagedBots,
runtimeMenuOpen,

View File

@ -51,6 +51,7 @@ export function useDashboardSupportData({
const [cronJobs, setCronJobs] = useState<CronJob[]>([]);
const [cronLoading, setCronLoading] = useState(false);
const [cronActionJobId, setCronActionJobId] = useState('');
const [cronActionType, setCronActionType] = useState<'' | 'starting' | 'stopping' | 'deleting'>('');
const [botSkills, setBotSkills] = useState<WorkspaceSkillOption[]>([]);
const [marketSkills, setMarketSkills] = useState<BotSkillMarketItem[]>([]);
const [isSkillUploading, setIsSkillUploading] = useState(false);
@ -79,6 +80,7 @@ export function useDashboardSupportData({
setCronJobs([]);
setCronLoading(false);
setCronActionJobId('');
setCronActionType('');
setBotSkills([]);
setMarketSkills([]);
setIsSkillUploading(false);
@ -391,9 +393,25 @@ export function useDashboardSupportData({
}
}, []);
const startCronJob = useCallback(async (jobId: string) => {
if (!selectedBotId || !jobId) return;
setCronActionJobId(jobId);
setCronActionType('starting');
try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/start`);
await loadCronJobs(selectedBotId);
} catch (error: any) {
notify(error?.response?.data?.detail || t.cronStartFail, { tone: 'error' });
} finally {
setCronActionJobId('');
setCronActionType('');
}
}, [loadCronJobs, notify, selectedBotId, t.cronStartFail]);
const stopCronJob = useCallback(async (jobId: string) => {
if (!selectedBotId || !jobId) return;
setCronActionJobId(jobId);
setCronActionType('stopping');
try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/stop`);
await loadCronJobs(selectedBotId);
@ -401,6 +419,7 @@ export function useDashboardSupportData({
notify(error?.response?.data?.detail || t.cronStopFail, { tone: 'error' });
} finally {
setCronActionJobId('');
setCronActionType('');
}
}, [loadCronJobs, notify, selectedBotId, t.cronStopFail]);
@ -413,6 +432,7 @@ export function useDashboardSupportData({
});
if (!ok) return;
setCronActionJobId(jobId);
setCronActionType('deleting');
try {
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}`);
await loadCronJobs(selectedBotId);
@ -420,12 +440,14 @@ export function useDashboardSupportData({
notify(error?.response?.data?.detail || t.cronDeleteFail, { tone: 'error' });
} finally {
setCronActionJobId('');
setCronActionType('');
}
}, [confirm, loadCronJobs, notify, selectedBotId, t.cronDelete, t.cronDeleteConfirm, t.cronDeleteFail]);
return {
botSkills,
cronActionJobId,
cronActionType,
cronJobs,
cronLoading,
createEnvParam,
@ -451,6 +473,7 @@ export function useDashboardSupportData({
resetSupportState,
saveBotEnvParams,
saveSingleEnvParam,
startCronJob,
setTopicFeedTopicKey,
stopCronJob,
topicFeedDeleteSavingById,

View File

@ -47,6 +47,7 @@ export function buildEnvParamsModalProps(options: {
export function buildCronJobsModalProps(options: {
cronActionJobId: string | null;
cronActionType?: 'starting' | 'stopping' | 'deleting' | '';
cronJobs: any[];
cronLoading: boolean;
deleteCronJob: (jobId: string) => Promise<void>;
@ -55,11 +56,13 @@ export function buildCronJobsModalProps(options: {
loadCronJobs: (botId: string) => Promise<void>;
onClose: () => void;
selectedBot?: any;
startCronJob: (jobId: string) => Promise<void>;
stopCronJob: (jobId: string) => Promise<void>;
open: boolean;
}): ComponentProps<typeof CronJobsModal> {
const {
cronActionJobId,
cronActionType,
cronJobs,
cronLoading,
deleteCronJob,
@ -68,6 +71,7 @@ export function buildCronJobsModalProps(options: {
loadCronJobs,
onClose,
selectedBot,
startCronJob,
stopCronJob,
open,
} = options;
@ -77,11 +81,13 @@ export function buildCronJobsModalProps(options: {
cronLoading,
cronJobs,
cronActionJobId: cronActionJobId || '',
cronActionType: cronActionType || '',
isZh,
labels,
formatCronSchedule,
onClose,
onReload: () => selectedBot && loadCronJobs(selectedBot.id),
onStartJob: startCronJob,
onStopJob: stopCronJob,
onDeleteJob: deleteCronJob,
};